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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 32 additions & 3 deletions apps/backend/agents/runtime/direct_api_autonomy.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,15 @@
from __future__ import annotations

import json
import logging
import os
from collections.abc import Mapping
from dataclasses import asdict, dataclass
from datetime import UTC, datetime, timedelta
from pathlib import Path
from typing import Any

from core.autonomy_level import AUTONOMY_LEVEL_ENV, resolve_autonomy_settings
from core.autonomy_policy import (
DEFAULT_ALLOWED_PHASES,
DEFAULT_MAX_HISTORY_AGE_DAYS,
Expand All @@ -33,6 +35,8 @@
resolve_provider_smoke_history_path,
)

logger = logging.getLogger(__name__)

DIRECT_API_AUTONOMOUS_ENV = "AUTO_CODE_DIRECT_API_FULL_AUTONOMOUS"
# Backward-compat aliases. Prefer ``AutonomyPolicy`` for runtime decisions.
DIRECT_API_AUTONOMOUS_PROVIDERS = DIRECT_API_PROVIDERS
Expand Down Expand Up @@ -112,13 +116,38 @@ def resolve_direct_api_autonomous_gate(
missing_requirements=["coding_phase"],
history_path=history_path_str,
)
if not direct_api_autonomous_env_enabled(env):
# Bridge the single AUTO_CODE_AUTONOMY knob (ADR-006) into the gate.
# ``resolve_autonomy_settings`` already folds the level default and the
# deprecated ``AUTO_CODE_DIRECT_API_FULL_AUTONOMOUS`` override into one
# flag, so safe/bold enable the gate without the legacy env var while
# that env var keeps working for back-compat.
autonomy = resolve_autonomy_settings(env=env)
if not autonomy.direct_api_gate_enabled:
return DirectApiAutonomousGate(
provider=provider,
allowed=False,
status="disabled",
reason="direct_api_autonomous_env_disabled",
missing_requirements=[DIRECT_API_AUTONOMOUS_ENV],
reason="direct_api_autonomous_disabled",
missing_requirements=[AUTONOMY_LEVEL_ENV],
history_path=history_path_str,
)
if autonomy.direct_api_skip_gate:
# bold is the power-user level: it trusts the operator and grants
# promotion without provider e2e evidence. Surface that loudly so
# an unproven provider running full-autonomous is never silent.
logger.warning(
"Direct-API autonomous gate bypassed for %s via %s=%s: promotion "
"granted WITHOUT provider e2e evidence (power-user mode).",
provider,
AUTONOMY_LEVEL_ENV,
autonomy.level.value,
)
return DirectApiAutonomousGate(
provider=provider,
allowed=True,
status="passed",
reason="direct_api_autonomous_gate_skipped",
missing_requirements=[],
history_path=history_path_str,
)

Expand Down
18 changes: 9 additions & 9 deletions apps/backend/cli/runtime_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -2344,15 +2344,15 @@ def build_runtime_modes_payload(
"provider as autonomous."
),
"direct_api_autonomous_runtime": (
f"Set {DIRECT_API_AUTONOMOUS_ENV}=true only after the provider "
"autonomous readiness and promotion gates are clean; the coder "
"phase can then use the direct_api_autonomous adapter. QA "
"phases (qa_reviewer / qa_fixer) still require Claude + "
"full_autonomous and will fail fast on any direct provider "
"until Phase 1.1 / 1.2 / 1.3 capability work (MCP execution, "
"mutating subagents, sandbox enforcement) is wired into the "
"QA runtime path. Prefer AUTO_CODE_AUTONOMY=safe (see "
"ADR-006); the legacy env var still works but is deprecated."
"Set AUTO_CODE_AUTONOMY=safe to let the coder phase use the "
"direct_api_autonomous adapter once the provider readiness and "
"promotion gates are clean (AUTO_CODE_AUTONOMY=bold skips the "
"evidence gate for power users). The legacy "
f"{DIRECT_API_AUTONOMOUS_ENV}=true still enables it but is "
"deprecated (see ADR-006). QA phases (qa_reviewer / qa_fixer) "
"still require Claude + full_autonomous and will fail fast on "
"any direct provider until the QA runtime path gains the MCP "
"execution, mutating subagents, and sandbox capabilities."
Comment on lines +2347 to +2355

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.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Align the QA guidance with the actual qa_fixer policy.

This text says qa_reviewer / qa_fixer still require Claude + full_autonomous, but the runtime policy in this file still allows the direct-API autonomous path for qa_fixer when the gate passes. Please either update the guidance to call out qa_reviewer only, or tighten the policy so qa_fixer is blocked too.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/backend/cli/runtime_commands.py` around lines 2347 - 2355, The comment
mismatches the runtime policy: either update the guidance string to mention only
qa_reviewer or change the policy to also block qa_fixer from the direct-API
autonomous path; locate the long help string that mentions AUTO_CODE_AUTONOMY,
DIRECT_API_AUTONOMOUS_ENV and the sentence "QA phases (qa_reviewer / qa_fixer)
still require Claude + full_autonomous…" and either remove qa_fixer from that
parenthetical (if you keep current policy) or, if you prefer to tighten policy,
update the runtime policy check that permits direct-API for qa_fixer (the
routine that evaluates phase/role autonomy) to deny qa_fixer the direct-API
autonomous path so the text and policy remain consistent.

),
"autonomy_level": (
"Set AUTO_CODE_AUTONOMY=off|claude|safe|bold as the single "
Expand Down
84 changes: 83 additions & 1 deletion tests/test_agent_runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -742,6 +742,7 @@ def test_direct_api_autonomous_gate_requires_env_and_clean_history(
):
_write_direct_api_autonomous_history(tmp_path)
monkeypatch.delenv(DIRECT_API_AUTONOMOUS_ENV, raising=False)
monkeypatch.delenv("AUTO_CODE_AUTONOMY", raising=False)

disabled_gate = resolve_direct_api_autonomous_gate(
provider_name="openai",
Expand All @@ -751,7 +752,7 @@ def test_direct_api_autonomous_gate_requires_env_and_clean_history(

assert disabled_gate.allowed is False
assert disabled_gate.status == "disabled"
assert disabled_gate.reason == "direct_api_autonomous_env_disabled"
assert disabled_gate.reason == "direct_api_autonomous_disabled"

monkeypatch.setenv(DIRECT_API_AUTONOMOUS_ENV, "true")
passed_gate = resolve_direct_api_autonomous_gate(
Expand All @@ -767,6 +768,87 @@ def test_direct_api_autonomous_gate_requires_env_and_clean_history(
assert passed_gate.missing_requirements == []


def test_direct_api_autonomous_gate_enabled_via_autonomy_safe(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
):
"""AUTO_CODE_AUTONOMY=safe enables the gate without the legacy env var."""
_write_direct_api_autonomous_history(tmp_path)
monkeypatch.delenv(DIRECT_API_AUTONOMOUS_ENV, raising=False)
monkeypatch.setenv("AUTO_CODE_AUTONOMY", "safe")

gate = resolve_direct_api_autonomous_gate(
provider_name="openai",
project_dir=tmp_path,
phase="coding",
)

assert gate.allowed is True
assert gate.status == "passed"
assert gate.reason == "direct_api_autonomous_gate_passed"


def test_direct_api_autonomous_gate_safe_still_requires_evidence(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
):
"""safe enables the gate but evidence is still mandatory."""
monkeypatch.delenv(DIRECT_API_AUTONOMOUS_ENV, raising=False)
monkeypatch.setenv("AUTO_CODE_AUTONOMY", "safe")

gate = resolve_direct_api_autonomous_gate(
provider_name="openai",
project_dir=tmp_path,
phase="coding",
)

assert gate.allowed is False
assert gate.reason == "provider_history_missing"
assert gate.missing_requirements == ["provider_e2e_history"]


def test_direct_api_autonomous_gate_bold_skips_evidence(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
):
"""bold is the power-user level: promotion is granted without evidence."""
monkeypatch.delenv(DIRECT_API_AUTONOMOUS_ENV, raising=False)
monkeypatch.setenv("AUTO_CODE_AUTONOMY", "bold")

# No history artifact exists at all, yet bold still grants.
gate = resolve_direct_api_autonomous_gate(
provider_name="openai",
project_dir=tmp_path,
phase="coding",
)

assert gate.allowed is True
assert gate.status == "passed"
assert gate.reason == "direct_api_autonomous_gate_skipped"
assert gate.missing_requirements == []


def test_direct_api_autonomous_gate_disabled_on_claude_level(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
):
"""claude/off never enable direct-API autonomy, regardless of evidence."""
_write_direct_api_autonomous_history(tmp_path)
monkeypatch.delenv(DIRECT_API_AUTONOMOUS_ENV, raising=False)
monkeypatch.setenv("AUTO_CODE_AUTONOMY", "claude")

gate = resolve_direct_api_autonomous_gate(
provider_name="openai",
project_dir=tmp_path,
phase="coding",
)

assert gate.allowed is False
assert gate.status == "disabled"
assert gate.reason == "direct_api_autonomous_disabled"
assert gate.missing_requirements == ["AUTO_CODE_AUTONOMY"]


def test_direct_api_autonomous_gate_blocks_missing_corrupt_and_stale_history(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
Expand Down
Loading