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
This commit is contained in:
@@ -0,0 +1,163 @@
|
||||
"""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,
|
||||
)
|
||||
Reference in New Issue
Block a user