mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
fix(infra): mypy 2.3 compat; _ReleaseContext.prod_branch; deny bare uv sync; telegram compose flag
Renames the smoke-replay loop variable mypy 2.3's stricter narrowing rejects (the uv.lock bump made this the promotion blocker), renames _ReleaseContext.default_branch to prod_branch to match what it holds since the env ladder, extends the Makefile-gated guard to bare uv sync on both runtimes (shared-cache poisoning is the race the guard exists for), and lists ROBOCO_TELEGRAM_ENABLED in both compose files (byte-identical).
This commit is contained in:
@@ -529,6 +529,10 @@ services:
|
|||||||
ROBOCO_RELEASE_MANAGER_ENABLED: ${ROBOCO_RELEASE_MANAGER_ENABLED:-true}
|
ROBOCO_RELEASE_MANAGER_ENABLED: ${ROBOCO_RELEASE_MANAGER_ENABLED:-true}
|
||||||
ROBOCO_ORG_MEMORY_ENABLED: ${ROBOCO_ORG_MEMORY_ENABLED:-true}
|
ROBOCO_ORG_MEMORY_ENABLED: ${ROBOCO_ORG_MEMORY_ENABLED:-true}
|
||||||
ROBOCO_X_ENGINE_ENABLED: ${ROBOCO_X_ENGINE_ENABLED:-true}
|
ROBOCO_X_ENGINE_ENABLED: ${ROBOCO_X_ENGINE_ENABLED:-true}
|
||||||
|
# Telegram notifications bridge: best-effort DMs to the CEO on escalation
|
||||||
|
# + completion. Config default is OFF; not yet armed here (needs stored
|
||||||
|
# bot-token + chat-id credentials regardless of this flag).
|
||||||
|
ROBOCO_TELEGRAM_ENABLED: ${ROBOCO_TELEGRAM_ENABLED:-false}
|
||||||
ROBOCO_OBSIDIAN_VAULT_ENABLED: ${ROBOCO_OBSIDIAN_VAULT_ENABLED:-true}
|
ROBOCO_OBSIDIAN_VAULT_ENABLED: ${ROBOCO_OBSIDIAN_VAULT_ENABLED:-true}
|
||||||
ROBOCO_VAULT_PATH: ${ROBOCO_VAULT_PATH:-/app/vault}
|
ROBOCO_VAULT_PATH: ${ROBOCO_VAULT_PATH:-/app/vault}
|
||||||
ROBOCO_VAULT_INTAKE_ENABLED: ${ROBOCO_VAULT_INTAKE_ENABLED:-true}
|
ROBOCO_VAULT_INTAKE_ENABLED: ${ROBOCO_VAULT_INTAKE_ENABLED:-true}
|
||||||
|
|||||||
@@ -529,6 +529,10 @@ services:
|
|||||||
ROBOCO_RELEASE_MANAGER_ENABLED: ${ROBOCO_RELEASE_MANAGER_ENABLED:-true}
|
ROBOCO_RELEASE_MANAGER_ENABLED: ${ROBOCO_RELEASE_MANAGER_ENABLED:-true}
|
||||||
ROBOCO_ORG_MEMORY_ENABLED: ${ROBOCO_ORG_MEMORY_ENABLED:-true}
|
ROBOCO_ORG_MEMORY_ENABLED: ${ROBOCO_ORG_MEMORY_ENABLED:-true}
|
||||||
ROBOCO_X_ENGINE_ENABLED: ${ROBOCO_X_ENGINE_ENABLED:-true}
|
ROBOCO_X_ENGINE_ENABLED: ${ROBOCO_X_ENGINE_ENABLED:-true}
|
||||||
|
# Telegram notifications bridge: best-effort DMs to the CEO on escalation
|
||||||
|
# + completion. Config default is OFF; not yet armed here (needs stored
|
||||||
|
# bot-token + chat-id credentials regardless of this flag).
|
||||||
|
ROBOCO_TELEGRAM_ENABLED: ${ROBOCO_TELEGRAM_ENABLED:-false}
|
||||||
ROBOCO_OBSIDIAN_VAULT_ENABLED: ${ROBOCO_OBSIDIAN_VAULT_ENABLED:-true}
|
ROBOCO_OBSIDIAN_VAULT_ENABLED: ${ROBOCO_OBSIDIAN_VAULT_ENABLED:-true}
|
||||||
ROBOCO_VAULT_PATH: ${ROBOCO_VAULT_PATH:-/app/vault}
|
ROBOCO_VAULT_PATH: ${ROBOCO_VAULT_PATH:-/app/vault}
|
||||||
ROBOCO_VAULT_INTAKE_ENABLED: ${ROBOCO_VAULT_INTAKE_ENABLED:-true}
|
ROBOCO_VAULT_INTAKE_ENABLED: ${ROBOCO_VAULT_INTAKE_ENABLED:-true}
|
||||||
|
|||||||
@@ -393,7 +393,7 @@ fi
|
|||||||
# agent's command string, not subprocesses) and WorkspaceService's uv sync
|
# agent's command string, not subprocesses) and WorkspaceService's uv sync
|
||||||
# (subprocess, not the agent Bash tool) are untouched. On grok a deny cancels
|
# (subprocess, not the agent Bash tool) are untouched. On grok a deny cancels
|
||||||
# the whole run, so ROBOCO_GUARD_SKIP_PM=1 nudges (exit 0) instead.
|
# the whole run, so ROBOCO_GUARD_SKIP_PM=1 nudges (exit 0) instead.
|
||||||
if test -f Makefile && echo "$low" | grep -qE '(^|[[:space:];&|])(uv[[:space:]]+(run|pip[[:space:]]+(install|uninstall)|lock|add|remove)|pip3?[[:space:]]+(install|uninstall)|conda[[:space:]]+(install|create|run)|poetry[[:space:]]+(run|install|add))([[:space:]]|$)'; then
|
if test -f Makefile && echo "$low" | grep -qE '(^|[[:space:];&|])(uv[[:space:]]+(run|sync|pip[[:space:]]+(install|uninstall)|lock|add|remove)|pip3?[[:space:]]+(install|uninstall)|conda[[:space:]]+(install|create|run)|poetry[[:space:]]+(run|install|add))([[:space:]]|$)'; then
|
||||||
if [ -n "${ROBOCO_GUARD_SKIP_PM:-}" ]; then
|
if [ -n "${ROBOCO_GUARD_SKIP_PM:-}" ]; then
|
||||||
echo "Nudge: raw package-manager commands are blocked — use \`make quality\` / \`make gate\` / \`make lint\` / \`make test\`. The Makefile sets UV_NO_SYNC=1 + a private cache; bare \`uv run\` bypasses that." >&2
|
echo "Nudge: raw package-manager commands are blocked — use \`make quality\` / \`make gate\` / \`make lint\` / \`make test\`. The Makefile sets UV_NO_SYNC=1 + a private cache; bare \`uv run\` bypasses that." >&2
|
||||||
exit 0
|
exit 0
|
||||||
|
|||||||
@@ -133,6 +133,7 @@ _DESTRUCTIVE_DENY = ("Bash(rm -rf*)",)
|
|||||||
# ROBOCO_GUARD_SKIP_PM=1 instead of canceling the run.
|
# ROBOCO_GUARD_SKIP_PM=1 instead of canceling the run.
|
||||||
_RAW_PM_DENY = (
|
_RAW_PM_DENY = (
|
||||||
"Bash(uv run*)",
|
"Bash(uv run*)",
|
||||||
|
"Bash(uv sync*)",
|
||||||
"Bash(uv pip install*)",
|
"Bash(uv pip install*)",
|
||||||
"Bash(uv pip uninstall*)",
|
"Bash(uv pip uninstall*)",
|
||||||
"Bash(uv lock*)",
|
"Bash(uv lock*)",
|
||||||
|
|||||||
@@ -261,7 +261,7 @@ class _ReleaseContext:
|
|||||||
"""Writable-clone coordinates for the production release ops."""
|
"""Writable-clone coordinates for the production release ops."""
|
||||||
|
|
||||||
slug: str
|
slug: str
|
||||||
default_branch: str
|
prod_branch: str
|
||||||
root: Path
|
root: Path
|
||||||
git_url: str
|
git_url: str
|
||||||
# Per-call ``-c http.extraheader=Authorization: Basic …`` prefix so the PAT
|
# Per-call ``-c http.extraheader=Authorization: Basic …`` prefix so the PAT
|
||||||
@@ -281,7 +281,7 @@ class _GitReleaseOps:
|
|||||||
def __init__(self, session: AsyncSession, ctx: _ReleaseContext) -> None:
|
def __init__(self, session: AsyncSession, ctx: _ReleaseContext) -> None:
|
||||||
self._session = session
|
self._session = session
|
||||||
self._slug = ctx.slug
|
self._slug = ctx.slug
|
||||||
self._default_branch = ctx.default_branch
|
self._default_branch = ctx.prod_branch
|
||||||
self._root = ctx.root
|
self._root = ctx.root
|
||||||
self._git_url = ctx.git_url
|
self._git_url = ctx.git_url
|
||||||
self._git_prefix = ctx.git_prefix
|
self._git_prefix = ctx.git_prefix
|
||||||
@@ -556,7 +556,7 @@ async def get_release_executor(session: AsyncSession) -> ReleaseExecutor:
|
|||||||
root = await _prepare_release_clone(slug, git_url, git_prefix, default_branch)
|
root = await _prepare_release_clone(slug, git_url, git_prefix, default_branch)
|
||||||
ctx = _ReleaseContext(
|
ctx = _ReleaseContext(
|
||||||
slug=slug,
|
slug=slug,
|
||||||
default_branch=default_branch,
|
prod_branch=default_branch,
|
||||||
root=root,
|
root=root,
|
||||||
git_url=git_url,
|
git_url=git_url,
|
||||||
git_prefix=git_prefix,
|
git_prefix=git_prefix,
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ async def test_h24_wait_for_ci_polls_through_non_success(
|
|||||||
|
|
||||||
ctx = _ReleaseContext(
|
ctx = _ReleaseContext(
|
||||||
slug="roboco",
|
slug="roboco",
|
||||||
default_branch="master",
|
prod_branch="master",
|
||||||
root=Path("/tmp/release-e2e"),
|
root=Path("/tmp/release-e2e"),
|
||||||
git_url="",
|
git_url="",
|
||||||
git_prefix=[],
|
git_prefix=[],
|
||||||
|
|||||||
@@ -350,8 +350,8 @@ def _assert_spec_introspection(record: dict[str, Any]) -> None:
|
|||||||
f"the spec. Fix commit: {record['fix_commit']}"
|
f"the spec. Fix commit: {record['fix_commit']}"
|
||||||
)
|
)
|
||||||
all_assigned_verbs: set[str] = set()
|
all_assigned_verbs: set[str] = set()
|
||||||
for r in spec.Role:
|
for any_role in spec.Role:
|
||||||
all_assigned_verbs.update(spec.intents_for_role(r))
|
all_assigned_verbs.update(spec.intents_for_role(any_role))
|
||||||
for verb_name in spec._INTENT_VERBS:
|
for verb_name in spec._INTENT_VERBS:
|
||||||
assert verb_name in all_assigned_verbs, (
|
assert verb_name in all_assigned_verbs, (
|
||||||
f"bug {bug_id} regressed: verb {verb_name!r} is declared in "
|
f"bug {bug_id} regressed: verb {verb_name!r} is declared in "
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ from roboco.llm.providers.grok_cli_config import _RAW_PM_DENY, _deny_rules
|
|||||||
def test_raw_pm_deny_rules_present() -> None:
|
def test_raw_pm_deny_rules_present() -> None:
|
||||||
rules = _deny_rules("developer")
|
rules = _deny_rules("developer")
|
||||||
assert "Bash(uv run*)" in rules
|
assert "Bash(uv run*)" in rules
|
||||||
|
assert "Bash(uv sync*)" in rules
|
||||||
assert "Bash(uv pip install*)" in rules
|
assert "Bash(uv pip install*)" in rules
|
||||||
assert "Bash(pip install*)" in rules
|
assert "Bash(pip install*)" in rules
|
||||||
assert "Bash(conda install*)" in rules
|
assert "Bash(conda install*)" in rules
|
||||||
|
|||||||
@@ -23,13 +23,14 @@ _DENIED = 2
|
|||||||
_ALLOWED = 0
|
_ALLOWED = 0
|
||||||
|
|
||||||
|
|
||||||
def _run(cmd: str) -> int:
|
def _run(cmd: str, cwd: Path | None = None) -> int:
|
||||||
payload = json.dumps({"tool_name": "Bash", "tool_input": {"command": cmd}})
|
payload = json.dumps({"tool_name": "Bash", "tool_input": {"command": cmd}})
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
[str(GUARD)],
|
[str(GUARD)],
|
||||||
input=payload,
|
input=payload,
|
||||||
capture_output=True,
|
capture_output=True,
|
||||||
text=True,
|
text=True,
|
||||||
|
cwd=str(cwd) if cwd else None,
|
||||||
check=False,
|
check=False,
|
||||||
)
|
)
|
||||||
return result.returncode
|
return result.returncode
|
||||||
@@ -432,14 +433,22 @@ def test_blocks_app_mutation_even_in_grok_mode() -> None:
|
|||||||
assert result.returncode == _DENIED
|
assert result.returncode == _DENIED
|
||||||
|
|
||||||
|
|
||||||
def test_allows_uv_sync_in_workspace() -> None:
|
def test_denies_uv_sync_in_workspace_when_makefile_present() -> None:
|
||||||
"""Legit dependency sync in the agent's own workspace clone must pass."""
|
"""W1: bare ``uv sync`` uses the shared ~/.cache/uv instead of the Makefile's
|
||||||
|
private UV_CACHE_DIR — the cache-poisoning race the guard exists to
|
||||||
|
prevent. Makefile-gated like ``uv run``: use ``uv sync --extra dev`` via
|
||||||
|
``make`` targets, not directly."""
|
||||||
assert (
|
assert (
|
||||||
_run("cd /data/workspaces/roboco/backend/be-dev-1 && uv sync --extra dev")
|
_run("cd /data/workspaces/roboco/backend/be-dev-1 && uv sync --extra dev")
|
||||||
== _ALLOWED
|
== _DENIED
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_allows_uv_sync_in_workspace_without_makefile(tmp_path: Path) -> None:
|
||||||
|
"""Makefile-less projects skip the deny — bare ``uv sync`` still passes."""
|
||||||
|
assert _run("uv sync --extra dev", cwd=tmp_path) == _ALLOWED
|
||||||
|
|
||||||
|
|
||||||
def test_denies_pip_install_when_makefile_present() -> None:
|
def test_denies_pip_install_when_makefile_present() -> None:
|
||||||
"""W1: raw ``pip install`` is Makefile-gated. A workspace clone carries
|
"""W1: raw ``pip install`` is Makefile-gated. A workspace clone carries
|
||||||
a ``Makefile`` (same repo), so a bare ``pip install`` is denied — agents
|
a ``Makefile`` (same repo), so a bare ``pip install`` is denied — agents
|
||||||
@@ -454,10 +463,15 @@ def test_allows_reading_files_under_app() -> None:
|
|||||||
assert _run("ls -la /app/.venv/bin") == _ALLOWED
|
assert _run("ls -la /app/.venv/bin") == _ALLOWED
|
||||||
|
|
||||||
|
|
||||||
def test_allows_uv_sync_for_app_named_workspace_project() -> None:
|
def test_allows_uv_sync_for_app_named_workspace_project(tmp_path: Path) -> None:
|
||||||
"""A workspace path that merely contains 'app' (e.g. .../myapp/...) must not
|
"""A workspace path that merely contains 'app' (e.g. .../myapp/...) must not
|
||||||
trip the rule — the boundary requires /app to be its own path segment."""
|
trip the /app-boundary rule — the boundary requires /app to be its own path
|
||||||
assert _run("cd /data/workspaces/myapp/backend/be-dev-1 && uv sync") == _ALLOWED
|
segment. Run outside a Makefile-having cwd so the W1 raw-uv-sync gate
|
||||||
|
(a separate rule, covered above) doesn't also fire here."""
|
||||||
|
assert (
|
||||||
|
_run("cd /data/workspaces/myapp/backend/be-dev-1 && uv sync", cwd=tmp_path)
|
||||||
|
== _ALLOWED
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -311,7 +311,7 @@ async def test_wait_for_ci_scoped_to_release_commit_not_branch_latest(
|
|||||||
|
|
||||||
ctx = _ReleaseContext(
|
ctx = _ReleaseContext(
|
||||||
slug="roboco-api",
|
slug="roboco-api",
|
||||||
default_branch="master",
|
prod_branch="master",
|
||||||
root=tmp_path,
|
root=tmp_path,
|
||||||
git_url="x",
|
git_url="x",
|
||||||
git_prefix=[],
|
git_prefix=[],
|
||||||
@@ -362,7 +362,7 @@ async def test_wait_for_ci_polls_through_rerun(
|
|||||||
|
|
||||||
ctx = _ReleaseContext(
|
ctx = _ReleaseContext(
|
||||||
slug="roboco-api",
|
slug="roboco-api",
|
||||||
default_branch="master",
|
prod_branch="master",
|
||||||
root=tmp_path,
|
root=tmp_path,
|
||||||
git_url="x",
|
git_url="x",
|
||||||
git_prefix=[],
|
git_prefix=[],
|
||||||
@@ -409,7 +409,7 @@ async def test_wait_for_ci_exhausts_window_on_persistent_failure(
|
|||||||
|
|
||||||
ctx = _ReleaseContext(
|
ctx = _ReleaseContext(
|
||||||
slug="roboco-api",
|
slug="roboco-api",
|
||||||
default_branch="master",
|
prod_branch="master",
|
||||||
root=tmp_path,
|
root=tmp_path,
|
||||||
git_url="x",
|
git_url="x",
|
||||||
git_prefix=[],
|
git_prefix=[],
|
||||||
@@ -538,7 +538,7 @@ async def test_release_push_argv_uses_extraheader_not_url_token(
|
|||||||
|
|
||||||
ctx = _ReleaseContext(
|
ctx = _ReleaseContext(
|
||||||
slug="roboco-api",
|
slug="roboco-api",
|
||||||
default_branch="master",
|
prod_branch="master",
|
||||||
root=tmp_path,
|
root=tmp_path,
|
||||||
git_url=git_url,
|
git_url=git_url,
|
||||||
git_prefix=git_prefix,
|
git_prefix=git_prefix,
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ from roboco.services.release_executor import _GitReleaseOps, _ReleaseContext
|
|||||||
def _ctx() -> _ReleaseContext:
|
def _ctx() -> _ReleaseContext:
|
||||||
return _ReleaseContext(
|
return _ReleaseContext(
|
||||||
slug="roboco",
|
slug="roboco",
|
||||||
default_branch="master",
|
prod_branch="master",
|
||||||
root=Path("/tmp/roboco-release-f012"),
|
root=Path("/tmp/roboco-release-f012"),
|
||||||
git_url="https://github.com/o/roboco",
|
git_url="https://github.com/o/roboco",
|
||||||
git_prefix=[],
|
git_prefix=[],
|
||||||
@@ -32,7 +32,7 @@ class _FakeGitOps(_GitReleaseOps):
|
|||||||
# Bypass the real __init__ (no session needed) — we only exercise
|
# Bypass the real __init__ (no session needed) — we only exercise
|
||||||
# commit_and_push, which calls self._git.
|
# commit_and_push, which calls self._git.
|
||||||
self._slug = ctx.slug
|
self._slug = ctx.slug
|
||||||
self._default_branch = ctx.default_branch
|
self._default_branch = ctx.prod_branch
|
||||||
self._root = ctx.root
|
self._root = ctx.root
|
||||||
self._git_url = ctx.git_url
|
self._git_url = ctx.git_url
|
||||||
self._git_prefix = ctx.git_prefix
|
self._git_prefix = ctx.git_prefix
|
||||||
|
|||||||
Reference in New Issue
Block a user