2026-05-16 03:45:02 +02:00
|
|
|
"""#167: agents could never Edit/Write any file (smoke-10..14).
|
|
|
|
|
|
|
|
|
|
Root cause: _generate_agent_settings put ``Write(*)``/``Edit(*)`` in the
|
|
|
|
|
GLOBAL base_deny. Claude Code evaluates rules deny -> ask -> allow and the
|
|
|
|
|
first match wins, so a deny ALWAYS beats a more-specific allow (the glob
|
|
|
|
|
syntax has no negation). The global deny therefore unconditionally
|
|
|
|
|
shadowed every per-role workspace-scoped Write/Edit allow — every agent,
|
|
|
|
|
developers included, got "Edit exists but is not enabled in this context"
|
|
|
|
|
and fell back to destructive bash redirection (clobbering real files;
|
|
|
|
|
e.g. a 207-line README rewritten to a 3-line stub, which QA correctly
|
|
|
|
|
failed).
|
|
|
|
|
|
|
|
|
|
Second defect: the workspace allow used a SINGLE leading slash
|
|
|
|
|
(``Write(/data/...)``). Claude Code resolves a single ``/`` against the
|
|
|
|
|
settings.json project root, not the container filesystem root, so even
|
|
|
|
|
without the global deny the allow never matched. Absolute container
|
|
|
|
|
paths require the ``//`` form.
|
|
|
|
|
|
|
|
|
|
Fix: drop Write(*)/Edit(*) from base_deny (roles that must not write keep
|
|
|
|
|
their OWN Write(*)/Edit(*) deny); emit the workspace allow in the ``//``
|
|
|
|
|
absolute form.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
import json
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
from unittest.mock import patch
|
|
|
|
|
|
|
|
|
|
from roboco.runtime.orchestrator import AgentOrchestrator
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _orch() -> AgentOrchestrator:
|
|
|
|
|
with patch.object(AgentOrchestrator, "__init__", return_value=None):
|
|
|
|
|
return AgentOrchestrator.__new__(AgentOrchestrator)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
_WS = "/data/workspaces/roboco-api/backend/be-dev-1"
|
|
|
|
|
_CELL = "/data/workspaces/roboco-api/backend"
|
|
|
|
|
|
|
|
|
|
_WRITER_ROLES = ("developer", "documenter", "product_owner", "head_marketing")
|
|
|
|
|
_NON_WRITER_ROLES = ("qa", "cell_pm", "main_pm", "auditor")
|
|
|
|
|
|
|
|
|
|
|
2026-07-02 05:46:01 +02:00
|
|
|
def test_generated_settings_cap_bash_output() -> None:
|
|
|
|
|
"""Agent settings carry an explicit Bash-output cap — a gate/test dump
|
|
|
|
|
enters context once and is re-read at cache-read price every later turn."""
|
|
|
|
|
orch = _orch()
|
|
|
|
|
path = orch._generate_agent_settings(
|
|
|
|
|
agent_id="be-dev-1",
|
|
|
|
|
role="developer",
|
|
|
|
|
workspace_path=_WS,
|
|
|
|
|
cell_workspace_path=_CELL,
|
|
|
|
|
)
|
|
|
|
|
settings = json.loads(Path(path).read_text())
|
|
|
|
|
assert settings["env"]["BASH_MAX_OUTPUT_LENGTH"] == "20000"
|
|
|
|
|
|
|
|
|
|
|
2026-05-16 03:45:02 +02:00
|
|
|
def test_generated_settings_base_deny_has_no_global_write_edit() -> None:
|
|
|
|
|
"""The settings file a developer is spawned with must NOT globally
|
|
|
|
|
deny Write/Edit (that shadowed the workspace allow → unusable)."""
|
|
|
|
|
orch = _orch()
|
|
|
|
|
path = orch._generate_agent_settings(
|
|
|
|
|
agent_id="be-dev-1",
|
|
|
|
|
role="developer",
|
|
|
|
|
workspace_path=_WS,
|
|
|
|
|
cell_workspace_path=_CELL,
|
|
|
|
|
)
|
|
|
|
|
settings = json.loads(Path(path).read_text())
|
|
|
|
|
deny = settings["permissions"]["deny"]
|
|
|
|
|
allow = settings["permissions"]["allow"]
|
|
|
|
|
|
|
|
|
|
assert "Write(*)" not in deny, deny
|
|
|
|
|
assert "Edit(*)" not in deny, deny
|
|
|
|
|
# The security denies that DO rely on deny-always-wins must remain.
|
|
|
|
|
assert "Bash(git:*)" in deny, deny
|
|
|
|
|
assert any(".git/config" in d for d in deny), deny
|
|
|
|
|
# Workspace allow present and in the // absolute form.
|
|
|
|
|
assert f"Write(/{_WS}/**)" in allow, allow
|
|
|
|
|
assert f"Edit(/{_WS}/**)" in allow, allow
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_writer_roles_use_double_slash_absolute_allow() -> None:
|
|
|
|
|
"""Every role that authors files emits Write/Edit allow rules in the
|
|
|
|
|
// absolute-filesystem form (single / silently never matches)."""
|
|
|
|
|
orch = _orch()
|
|
|
|
|
for role in _WRITER_ROLES:
|
|
|
|
|
perms = orch._get_role_permissions(
|
|
|
|
|
role=role, workspace_path=_WS, cell_workspace_path=_CELL
|
|
|
|
|
)
|
|
|
|
|
write_edit = [e for e in perms["allow"] if e.startswith(("Write(", "Edit("))]
|
|
|
|
|
assert write_edit, f"{role} should allow some Write/Edit: {perms}"
|
|
|
|
|
for entry in write_edit:
|
|
|
|
|
inner = entry[entry.index("(") + 1 :]
|
|
|
|
|
assert inner.startswith("//"), (
|
|
|
|
|
f"{role} allow rule must use // absolute form: {entry}"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_non_writer_roles_still_deny_write_edit() -> None:
|
|
|
|
|
"""Removing the GLOBAL deny must not let QA / PMs / auditor write —
|
|
|
|
|
they carry their own Write(*)/Edit(*) deny in the role config."""
|
|
|
|
|
orch = _orch()
|
|
|
|
|
for role in _NON_WRITER_ROLES:
|
|
|
|
|
perms = orch._get_role_permissions(
|
|
|
|
|
role=role, workspace_path=_WS, cell_workspace_path=_CELL
|
|
|
|
|
)
|
|
|
|
|
assert "Write(*)" in perms["deny"], f"{role}: {perms}"
|
|
|
|
|
assert "Edit(*)" in perms["deny"], f"{role}: {perms}"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_non_writer_generated_settings_block_write() -> None:
|
|
|
|
|
"""End-to-end: a cell_pm's generated settings still deny Write/Edit
|
|
|
|
|
(their own role deny survives the base_deny change)."""
|
|
|
|
|
orch = _orch()
|
|
|
|
|
path = orch._generate_agent_settings(
|
|
|
|
|
agent_id="be-pm",
|
|
|
|
|
role="cell_pm",
|
|
|
|
|
workspace_path=_WS,
|
|
|
|
|
cell_workspace_path=_CELL,
|
|
|
|
|
)
|
|
|
|
|
deny = json.loads(Path(path).read_text())["permissions"]["deny"]
|
|
|
|
|
assert "Write(*)" in deny, deny
|
|
|
|
|
assert "Edit(*)" in deny, deny
|