Skip to content

fix(agent-runner): reconcile an environment's LLM env (key, base URL, model) on every start - #653

Merged
pikann merged 1 commit into
Paca-AI:masterfrom
siedlegroup:fix/environment-llm-env-reconcile
Oct 28, 2026
Merged

pikann merged 1 commit into
Paca-AI:masterfrom
siedlegroup:fix/environment-llm-env-reconcile

Conversation

@kayahk

@kayahk kayahk commented Oct 28, 2026

Copy link
Copy Markdown
Contributor

Fixes #652.

Symptom: a conversation attached to a durable environment died at its first model call with executor: acp session/prompt: Authentication required, while the gateway recorded 401 KeyNotFoundError — the Pod was calling the provider with a key the agent row had stopped holding days earlier.

Cause: a static environment bakes the creating agent's LLM env at Pod creation. ensureEnvironmentEnv backfills it once and short-circuits forever after; ensureEnvironmentInfraEnv reconciles pacaInfraEnvKeys plus GOOSE_MODEL on every start but leaves the model API key and OPENAI_HOST frozen. So a rotated key, a moved base URL or a model change never reaches the Pod, and every later attach uses a credential that no longer exists.

Change

  • services/agent-runner/internal/sandbox/environment.go — new EnvironmentConfig.LLMKeyEnvVar: the provider's key env var name varies by provider (OPENAI_API_KEY, ANTHROPIC_API_KEY, …), so a backend cannot infer it from Env alone.
  • services/agent-runner/internal/sandbox/{k8s,docker}/environment.go — infraEnvKeysFor now also returns OPENAI_HOST and cfg.LLMKeyEnvVar next to GOOSE_MODEL, so both backends reconcile them on every StartEnvironment (set, update, or remove when the agent no longer has the value). Still gated on a matching GOOSE_PROVIDER: another provider's model id, base URL or key var is meaningless to the Pod's frozen provider, so that case is unchanged.
  • services/agent-runner/internal/executor/executor.go — pass the key var name from resolveProviderEnv, empty for a provider_cli agent (which has no model key of its own).
  • Comments on pacaInfraEnvKeys, StartEnvironment's Env contract and infraEnvKeysFor updated to match; PACA_AGENT_ID/PACA_PROJECT_ID stay deliberately frozen.

Tests (internal/sandbox/k8s/environment_test.go)

  • TestEnsureEnvironmentInfraEnv_RefreshesSameProviderLLMKeys: a Pod holding sk-revoked + an old model + the gateway base URL gets the agent's current key, model and host, with a user env var (GITHUB_TOKEN) left alone.
  • TestEnsureEnvironmentInfraEnv_LeavesLLMKeysWhenProviderDiffers: another provider's GOOSE_MODEL/key var/base URL are not applied — the PACA_* keys still reconcile.

Verification (local, services/agent-runner)

  • go build ./..., go vet ./..., gofmt -l clean; go test -count=1 ./internal/sandbox/... ./internal/executor/... passes.
  • Sabotage run (the extension reverted to GOOSE_MODEL only) fails on exactly the reported failure mode: deployment env["OPENAI_API_KEY"] = "sk-revoked", want "sk-fresh" plus the stale OPENAI_HOST.
  • golangci-lint is not installed on this workstation — CI is the gate for it.

Remaining limit (unchanged by this PR): GOOSE_PROVIDER itself is still frozen to the first agent, so two agents with different providers cannot share one environment; the second one's attach still runs against the first one's provider. That is the frozen-identity design, not something this fix silently changes — worth its own decision if environments are meant to serve a whole agent fleet.

A static environment bakes the creating agent's GOOSE_MODEL, model API key
and OPENAI_HOST into its Pod and, apart from GOOSE_MODEL, never touches
them again. A rotated key or an edited model therefore never reaches the
Pod: it keeps calling the provider with a credential the agent row no
longer holds, and every attach dies with ACP "session/prompt:
Authentication required".

Add GOOSE_MODEL's two siblings to the keys ensureEnvironmentInfraEnv
already reconciles on every StartEnvironment — OPENAI_HOST and the
provider's own key var, whose name varies by provider and so comes in as
the new EnvironmentConfig.LLMKeyEnvVar from the executor. Still gated on
a matching GOOSE_PROVIDER: a model id, base URL or key var belonging to
another provider's agent is meaningless to the one the Pod is frozen to,
so that case stays as it was.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

ℹ️ No blocking issues — a couple of suggestions inline.

Reviewed changes

  • EnvironmentConfig.LLMKeyEnvVar — new field naming the provider's model-key env var, since the name varies by provider and can't be inferred from Env alone.
  • infraEnvKeysFor (docker + k8s) — appends OPENAI_HOST and LLMKeyEnvVar to the reconciled keys when the frozen GOOSE_PROVIDER matches, so a rotated key, moved base URL or edited model follows the attaching agent instead of staying frozen.
  • coldStartEnvironment — populates LLMKeyEnvVar from resolveProviderEnv, empty for provider_cli.
  • Tests — k8s coverage for same-provider refresh and different-provider preservation.

ℹ️ Per-agent key reconciliation restarts a shared environment on alternating attaches

Because the model key now follows each attaching agent, an environment attached by two agents that share GOOSE_PROVIDER but hold different keys will patch its Pod template (k8s) or recreate its container (docker) on every alternating attach, restarting whatever it was serving. This extends the existing GOOSE_MODEL behavior rather than introducing a new mechanism, and the PR already calls out the different-provider case — worth confirming that same-provider, different-key environments are intended to serve only one agent at a time.

Technical details
# Per-agent key reconciliation across attaches

## Affected sites
- services/agent-runner/internal/sandbox/docker/environment.go:573 — key/OPENAI_HOST added to reconcile keys
- services/agent-runner/internal/sandbox/k8s/environment.go:599 — same
- services/agent-runner/internal/sandbox/docker/environment.go:612 — stale env triggers a full container recreate
- services/agent-runner/internal/sandbox/k8s/environment.go:692 — stale env patches the pod template (rolling restart)

## Open questions for the human
- Are same-provider, different-key agents ever expected to attach to one environment? If not, no action needed. If yes, the key can't be a per-attach reconciled value without thrash; it would need to be part of the frozen identity (like `GOOSE_PROVIDER`/`PACA_AGENT_ID`).

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏

// The model key's env var NAME varies by provider, so a backend cannot
// infer it from Env alone — see EnvironmentConfig.LLMKeyEnvVar.
keyEnvVar := ""
if cfg.AgentType != agent.AgentTypeProviderCLI {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The provider_cli branch also has a native auth key: buildProviderCLIContainerEnv writes ANTHROPIC_API_KEY/GEMINI_API_KEY/OPENAI_API_KEY from CLIAPIKeySecret when CLIAuthMode == "api_key". Leaving LLMKeyEnvVar empty for these agents keeps a rotated CLI key frozen on the Pod — the same failure this PR fixes for LLM agents.

Technical details
# provider_cli native key is not reconciled

## Affected sites
- services/agent-runner/internal/executor/executor.go:798 — LLMKeyEnvVar forced empty for provider_cli
- services/agent-runner/internal/executor/executor.go:501-515 — buildProviderCLIContainerEnv sets the adapter's native key env var from CLIAPIKeySecret
- services/agent-runner/internal/executor/providercli/*.go — APIKeyEnvVar() (claude-code→ANTHROPIC_API_KEY, codex→OPENAI_API_KEY, gemini-cli→GEMINI_API_KEY)

## Required outcome
- A provider_cli agent in api_key auth mode has its native key env var reconciled on attach, like the LLM path.

## Suggested approach (optional)
- In coldStartEnvironment, when AgentType == provider_cli and CLIAuthMode == api_key, look up providercli.Get(cfg.CLIProvider).APIKeyEnvVar() and use it as LLMKeyEnvVar (same empty-string guard for unsupported adapters / non-api_key mode).

}

// envMapOf reads backendRef's Deployment container env into a map.
func envMapOf(t *testing.T, m *Manager, name string) map[string]string {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

envMapOf is a duplicate of the existing deploymentEnvMap helper just above (only the namespace source differs). Reuse deploymentEnvMap and drop this helper.

@pikann pikann left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM! Thank you for the contribution! 🚀

@pikann
pikann merged commit 20e069e into Paca-AI:master Oct 28, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Durable environment keeps the creating agent's frozen LLM env: rotated keys and model changes never reach it

2 participants