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
14 changes: 14 additions & 0 deletions core/agent_setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,10 @@
from core.harness.permissions import PermissionMode
from core.harness.policy import (
build_permission_engine,
describe_security_posture,
resolve_execution_security_profile,
)
from core.harness.sandbox import sandbox_backend
from core.harness.tools import default_coding_tools
from core.llm_runtime import get_workflow_provider
from core.providers.catalog import resolve_model_info
Expand Down Expand Up @@ -344,6 +346,18 @@ def build_agent_session(
execution_security_profile=resolved_security_profile,
)

# Record what is actually enforcing for this session. The four knobs
# interact, and an unattended `full_auto` run looks identical to an
# approval-gated one in the transcript — which is precisely the fact a
# reader needs when asking whether a rewritten tool call could have run.
logger.info(
"security posture: {}",
describe_security_posture(
resolved_security_profile,
sandbox_backend=sandbox_backend(),
),
)

# Stable system context is assembled once here. Skills are intentionally
# not flattened into this prompt: AgentSession resolves a fresh immutable
# Skill snapshot at each turn, giving every frontend hot reload without
Expand Down
52 changes: 52 additions & 0 deletions core/harness/policy.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,13 @@
)
from core.harness.sandbox import sandbox_enabled

__all__ = [
"build_permission_engine",
"describe_security_posture",
"resolve_execution_security_profile",
"resolve_permission_mode",
]


def resolve_permission_mode(
config_mode: str | PermissionMode | None = None,
Expand Down Expand Up @@ -122,6 +129,51 @@ def resolve_execution_security_profile(
)


def describe_security_posture(
profile: ExecutionSecurityProfile,
*,
sandbox_backend: str | None = None,
) -> dict[str, Any]:
"""One-line answer to "what is actually enforcing right now?".

Why this exists. The resolved posture is spread across four independent
knobs (mode, preset, sandbox, approval policy) that interact, and a reader
of a log cannot tell from any one of them whether the run was gated or
wide open. That matters more than usual here: the difference between an
unattended ``full_auto`` run and one with an approver is the difference
between a rewritten tool call executing and a rewritten tool call being
stopped, and the two look identical in a transcript.

The returned mapping is deliberately flat and string-friendly so it can be
dropped into a log line or a structured event without further shaping. It
reports facts; it does not judge them.
"""

preset = profile.access_preset.value if profile.access_preset else None
# "Unattended" means nobody will be consulted before a tool call runs — not
# merely that the approval policy says so. The legacy ``full_auto`` mode
# short-circuits the engine with an unconditional ALLOW while still
# reporting ``on_request``, so trusting the policy field alone would report
# the most permissive configuration as gated, which is the exact mistake
# this helper exists to prevent.
unattended = (
profile.approval_policy is ApprovalPolicy.NEVER
or profile.permission_mode is ExecutionPermissionMode.FULL_AUTO
)
return {
"permission_mode": profile.permission_mode.value,
"access_preset": preset or "legacy",
"command_sandbox": profile.command_sandbox,
"sandbox_backend": sandbox_backend or "unknown",
"filesystem_scope": profile.filesystem_scope.value,
"approval_policy": profile.approval_policy.value,
"permission_rule_count": len(profile.permission_rules),
# The single fact worth surfacing without a reader having to combine
# the others: nobody will be asked before a tool call runs.
"unattended": unattended,
}


def build_permission_engine(
security_config: Any | None,
*,
Expand Down
38 changes: 38 additions & 0 deletions tests/test_harness_policy.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
from core.harness.permissions import PermissionDecision, PermissionMode
from core.harness.policy import (
build_permission_engine,
describe_security_posture,
resolve_execution_security_profile,
resolve_permission_mode,
)
Expand Down Expand Up @@ -211,3 +212,40 @@ def test_invalid_config_action_raises(bad_action):
build_permission_engine(
_cfg(permissions={"write_file": {"*": bad_action}}), cwd="/w"
)


# --- posture reporting ------------------------------------------------------


def test_posture_reports_full_auto_as_unattended():
"""The one fact a reader needs: will anyone be asked before a tool runs?

``full_auto`` short-circuits the engine with an unconditional ALLOW while
still carrying an ``on_request`` approval policy, so a report that trusted
the policy field alone would label the most permissive configuration as
gated.
"""

profile = resolve_execution_security_profile(
None, default_mode=PermissionMode.FULL_AUTO
)
posture = describe_security_posture(profile, sandbox_backend="job")
assert posture["unattended"] is True
assert posture["permission_mode"] == "full_auto"
assert posture["sandbox_backend"] == "job"


@pytest.mark.parametrize("mode", [PermissionMode.DEFAULT, PermissionMode.PLAN])
def test_posture_reports_gated_modes_as_attended(mode):
profile = resolve_execution_security_profile(None, default_mode=mode)
assert describe_security_posture(profile)["unattended"] is False


def test_posture_reports_full_access_preset_and_counts_rules():
profile = resolve_execution_security_profile(
_cfg(permissions={"bash": {"git push *": "ask"}})
)
posture = describe_security_posture(profile)
assert posture["permission_rule_count"] == len(profile.permission_rules)
assert posture["access_preset"] in {"legacy", "full_access", "ask", "read_only"}
assert posture["sandbox_backend"] == "unknown"
11 changes: 10 additions & 1 deletion workflows/code_implementation_workflow.py
Original file line number Diff line number Diff line change
Expand Up @@ -904,7 +904,16 @@ async def on_retry_wait(message: str) -> None:
# approver so an `ask` becomes an interactive confirmation. An unknown
# value falls back to this legacy client's FULL_AUTO default.
security_cfg = getattr(get_runtime().config, "security", None)
permission_engine = build_permission_engine(security_cfg, cwd=code_directory)
# ``default_mode`` is passed explicitly rather than left to the
# signature default. This workflow is the one caller that intentionally
# runs unattended, so its mode should be readable at the call site
# instead of inherited from a parameter far away — and a reviewer
# grepping for "who runs with no approver?" gets an answer.
permission_engine = build_permission_engine(
security_cfg,
cwd=code_directory,
default_mode=PermissionMode.FULL_AUTO,
)
mode = permission_engine.mode
approval_cb = None
if mode is not PermissionMode.FULL_AUTO:
Expand Down
Loading