Files
Software Orchestrator 5769c1cd22 Initial commit: a0_software_orchestrator v1.0
- Auto-Registration-Bug behoben (register_project/get_project_id/resolve_project Trennung)
- 25 Tests gruen (Pytest)
- block_compactor-Tool refactored (Option B: Soft-Check statt Hard-Block)
- 4 Restbaustellen gefixt
- DB-Schema: plugin_settings-Tabelle hinzugefuegt
- 3 Schattenprojekte aus DB geloescht
- Plan v3 + Refactor-Plan + Worklog dokumentiert
2026-06-16 22:13:06 +00:00

164 lines
5.9 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Tool: enforce Plan Mode by checking current mode (DB-backed, global)."""
from __future__ import annotations
import json
from typing import Any, Dict
from helpers.tool import Tool, Response
from usr.plugins.a0_software_orchestrator.helpers.db_state_store import (
_db_error_handler, _db,
)
_DEFAULT_MODE = {
"mode": "planning_only",
"allowed_actions": [],
"blocked_actions": [
"code",
"deploy",
"destroy",
"push_to_main",
],
}
_MODE_POLICIES = {
"planning_only": {"allowed_actions": [], "blocked_actions": ["code", "deploy", "destroy", "push_to_main"]},
"implementation_allowed": {"allowed_actions": ["code", "test", "commit"], "blocked_actions": ["deploy", "destroy", "push_to_main"]},
"runtime_verification_allowed": {"allowed_actions": ["code", "test", "commit", "start_server"], "blocked_actions": ["deploy", "destroy"]},
"deployment_preparation_allowed": {"allowed_actions": ["code", "test", "commit", "deploy_staging"], "blocked_actions": ["deploy_production", "destroy"]},
"release_handoff_allowed": {"allowed_actions": ["deploy_production"], "blocked_actions": ["destroy"]},
"maintenance_allowed": {"allowed_actions": ["code", "test", "commit", "deploy"], "blocked_actions": ["destroy"]},
}
# Plan-Mode ist plugin-weit (Bug 7 Fix). Der Key lebt in plugin_settings,
# einer Tabelle ohne project_id/FK saubere Architektur statt
# project_id=0-Workaround in orch_kv (Migration 5).
_PLUGIN_MODE_KEY = "orchestrator_mode"
def _normalize_mode(mode_data: Any) -> Dict[str, Any]:
"""Stellt sicher, dass das gelesene Mode-Dict die erwarteten Keys hat."""
if not isinstance(mode_data, dict):
return dict(_DEFAULT_MODE)
out = dict(_DEFAULT_MODE)
out.update({
"mode": mode_data.get("mode", _DEFAULT_MODE["mode"]),
"allowed_actions": list(mode_data.get("allowed_actions") or []),
"blocked_actions": list(mode_data.get("blocked_actions") or []),
})
return out
@_db_error_handler
def _get_plugin_mode() -> Dict[str, Any]:
"""Liest den globalen Plan-Mode aus plugin_settings.
Returns _DEFAULT_MODE wenn kein Eintrag existiert.
"""
db = _db()
row = db.conn.execute(
"SELECT value_json FROM plugin_settings WHERE key = ?",
(_PLUGIN_MODE_KEY,),
).fetchone()
if not row:
return dict(_DEFAULT_MODE)
try:
return _normalize_mode(json.loads(row[0]))
except (json.JSONDecodeError, TypeError):
return dict(_DEFAULT_MODE)
@_db_error_handler
def _set_plugin_mode(mode_data: Dict[str, Any]) -> None:
"""Schreibt den globalen Plan-Mode in plugin_settings."""
db = _db()
db.conn.execute(
"INSERT INTO plugin_settings (key, value_json, updated_at) "
"VALUES (?, ?, datetime('now')) "
"ON CONFLICT(key) DO UPDATE SET "
" value_json = excluded.value_json, updated_at = datetime('now')",
(_PLUGIN_MODE_KEY, json.dumps(mode_data)),
)
db.conn.commit()
class PlanModeGuard(Tool):
"""Prüft den aktuellen Plan-Mode (plugin-weit) und entscheidet, ob eine
Aktion erlaubt ist.
KEIN resolve_project()-Aufruf mehr: Plan-Mode ist global, nicht pro Projekt.
Args (kwargs):
requested_action (str): Action-Name zum Testen (z.B. "code", "deploy").
new_mode (str): Optional neuen Mode setzen (für Mode-Transitions).
"""
async def execute(
self,
requested_action: str = "",
new_mode: str = "",
**kwargs,
):
# Mode-Transition (z.B. planning_only → implementation_allowed)
if new_mode:
if new_mode not in _MODE_POLICIES:
return Response(
message=json.dumps({
"verdict": "ERROR",
"error": f"invalid mode: {new_mode}",
"allowed_modes": sorted(_MODE_POLICIES),
}, indent=2),
break_loop=False,
)
mode_data = {
"mode": new_mode,
"allowed_actions": list(_MODE_POLICIES[new_mode]["allowed_actions"]),
"blocked_actions": list(_MODE_POLICIES[new_mode]["blocked_actions"]),
}
_set_plugin_mode(mode_data)
return Response(
message=json.dumps({
"verdict": "ALLOWED",
"mode": new_mode,
"allowed_actions": mode_data["allowed_actions"],
"blocked_actions": mode_data["blocked_actions"],
"transitioned": True,
}, indent=2),
break_loop=False,
)
# Ansonsten: aktuellen Mode lesen und Aktion prüfen
mode_data = _get_plugin_mode()
current_mode = mode_data["mode"]
allowed = mode_data["allowed_actions"]
blocked = mode_data["blocked_actions"]
if requested_action and requested_action in blocked:
return Response(
message=json.dumps({
"verdict": "BLOCKED",
"mode": current_mode,
"action": requested_action,
"reason": f"action '{requested_action}' is not allowed in mode '{current_mode}'",
}, indent=2),
break_loop=False,
)
if not requested_action or requested_action in allowed:
return Response(
message=json.dumps({
"verdict": "ALLOWED",
"mode": current_mode,
"action": requested_action,
}, indent=2),
break_loop=False,
)
return Response(
message=json.dumps({
"verdict": "UNKNOWN",
"mode": current_mode,
"action": requested_action,
"hint": f"action not in allowed={allowed} or blocked={blocked}",
}, indent=2),
break_loop=False,
)