from __future__ import annotations import json import os import re import subprocess import tempfile import threading import time import urllib.error import urllib.request import uuid from pathlib import Path from typing import Any METHOD_STATUS = "repository.status" SQL_LOCK_EVIDENCE = { "native_lock_state": "unknown", "native_lock_verification": "not_available_via_infobase_sql", "excluded_sources": { "_ConfigChngR": "exchange_plan_change_registration", "_ConfigChngR_ExtProps": "exchange_plan_changed_file_details", }, } METHOD_LOCK_PLAN = "repository.lock.plan" METHOD_LOCK_REQUEST = "repository.lock.request" METHOD_LOCK_REQUEST_STATUS = "repository.lock.request.status" METHOD_LOCK_REQUEST_CANCEL = "repository.lock.request.cancel" METHOD_LOCK = "repository.lock" METHOD_CONFIRM = "repository.lock.confirm" METHOD_VERIFY = "repository.lock.verify" METHOD_CLOSE = "repository.lock.close" METHOD_UNLOCK = "repository.unlock" METHOD_COMMIT_PLAN = "repository.commit.plan" METHOD_COMMIT = "repository.commit" METHODS = {METHOD_STATUS, METHOD_LOCK_PLAN, METHOD_LOCK_REQUEST, METHOD_LOCK_REQUEST_STATUS, METHOD_LOCK_REQUEST_CANCEL, METHOD_LOCK, METHOD_CONFIRM, METHOD_VERIFY, METHOD_CLOSE, METHOD_UNLOCK, METHOD_COMMIT_PLAN, METHOD_COMMIT} SUPPORTED_BACKENDS = {"direct", "karman_bridge"} SUPPORTED_LOCK_MODES = {"automatic", "manual"} SUPPORTED_REPOSITORY_MODES = {"none", "manual", "automatic", "unknown"} SUPPORTED_REPOSITORY_CONNECTION_STATES = {"not_configured", "configured", "unavailable", "unknown"} SUPPORTED_SUPPORT_MODES = {"none", "editable", "locked", "rules", "unknown"} _BASE_LOCKS: dict[str, threading.Lock] = {} _BASE_LOCKS_GUARD = threading.Lock() _STATE_LOCK = threading.RLock() def external_1c_enabled() -> bool: return str(os.environ.get("ONEC_ADAPTER_ENABLE_EXTERNAL_1C") or "").strip().casefold() in {"1", "true", "yes", "on"} def _base_lock(base_id: str, layer: str) -> threading.Lock: key = f"{base_id}:{layer}" with _BASE_LOCKS_GUARD: return _BASE_LOCKS.setdefault(key, threading.Lock()) def _load_json_map(env_name: str, file_env_name: str) -> tuple[dict[str, Any] | None, dict[str, Any] | None]: raw = os.environ.get(env_name) path = os.environ.get(file_env_name) if not raw and path: try: raw = Path(path).read_text(encoding="utf-8-sig") except Exception as exc: return None, {"status": "invalid_config", "message": f"Cannot read {file_env_name}: {exc}"} if not raw: return None, { "status": "not_configured", "message": f"Set {env_name} or {file_env_name} with an explicit entry for this base_id.", } try: value = json.loads(raw) except json.JSONDecodeError as exc: return None, {"status": "invalid_config", "message": f"{env_name} is not valid JSON: {exc}"} if not isinstance(value, dict): return None, {"status": "invalid_config", "message": f"{env_name} must be an object keyed by base_id."} return value, None def development_layer_id(payload: dict[str, Any] | None = None) -> str: value = payload if isinstance(payload, dict) else {} target = value.get("target") if isinstance(value.get("target"), dict) else {} origin = target.get("origin") if isinstance(target.get("origin"), dict) else {} origin_extension = origin.get("extension") if isinstance(origin.get("extension"), dict) else {} extension_guid = str(value.get("extension_guid") or target.get("extension_guid") or origin_extension.get("guid") or "").strip().lower() layer_id = str(value.get("layer_id") or target.get("layer_id") or "").strip().lower() if extension_guid: return f"extension:{extension_guid}" if layer_id.startswith("extension:"): return layer_id return "base" def _base_settings(base_id: str) -> tuple[dict[str, Any] | None, dict[str, Any] | None]: values, error = _load_json_map("ONEC_SQL_BASES_JSON", "ONEC_SQL_BASES_JSON_FILE") base_item = values.get(base_id) if values and isinstance(values.get(base_id), dict) else None if base_item is not None: return base_item, None return None, error or {"status": "not_configured", "message": f"No SQL configuration for base_id '{base_id}'."} def development_layer_config(base_id: str, layer_id: str) -> tuple[dict[str, Any] | None, dict[str, Any] | None]: base_item, base_error = _base_settings(base_id) if base_error: return None, base_error layers = base_item.get("development_layers") if isinstance(base_item.get("development_layers"), dict) else None if layers is not None: item = layers.get(layer_id) if not isinstance(item, dict): return None, {"status": "layer_not_configured", "message": f"No development layer configuration for '{layer_id}' in base_id '{base_id}'."} return dict(item), None if layer_id == "base" and isinstance(base_item.get("repository"), dict): return {"repository": dict(base_item["repository"]), "legacy": True}, None return None, {"status": "layer_not_configured", "message": f"No development layer configuration for '{layer_id}' in base_id '{base_id}'."} def repository_config(base_id: str, layer_id: str = "base") -> tuple[dict[str, Any] | None, dict[str, Any] | None]: base_item, _ = _base_settings(base_id) layer, layer_error = development_layer_config(base_id, layer_id) if layer_error is None: item = layer.get("repository") if item is None: return {"mode": "unknown", "lock_mode": "manual", "layer_id": layer_id, "layer": layer_id}, None else: item = None if isinstance(base_item, dict) and isinstance(base_item.get("development_layers"), dict): return None, layer_error # Legacy repository-only configuration remains readable for the base layer. if item is None and layer_id == "base" and isinstance(base_item, dict): item = base_item.get("repository") if item is None: values, repository_error = _load_json_map("ONEC_REPOSITORY_BASES_JSON", "ONEC_REPOSITORY_BASES_JSON_FILE") if repository_error and layer_error: return None, repository_error item = values.get(base_id) if values else None if item is None: return None, {"status": "not_configured", "message": f"No repository configuration for base_id '{base_id}'."} if not isinstance(item, dict): return None, {"status": "invalid_config", "message": f"Repository configuration for '{base_id}' must be an object."} configured = dict(item) configured["mode"] = str(configured.get("mode") or configured.get("lock_mode") or "automatic").strip().casefold() if configured["mode"] not in SUPPORTED_REPOSITORY_MODES: return None, {"status": "invalid_config", "message": "repository mode must be none, manual, automatic, or unknown."} default_connection_state = "unknown" if configured["mode"] == "unknown" else ("not_configured" if configured["mode"] == "none" else "configured") configured["connection_state"] = str(configured.get("connection_state") or configured.get("repository_connection") or configured.get("connection") or default_connection_state).strip().casefold() if configured["connection_state"] not in SUPPORTED_REPOSITORY_CONNECTION_STATES: return None, {"status": "invalid_config", "message": "repository connection_state must be not_configured, configured, unavailable, or unknown."} if configured["mode"] == "unknown": return {"mode": "unknown", "lock_mode": "manual", "connection_state": configured["connection_state"], "layer_id": layer_id, "layer": layer_id}, None if configured["mode"] == "none": return {"mode": "none", "lock_mode": "manual", "connection_state": configured["connection_state"], "layer_id": layer_id, "layer": layer_id}, None configured["backend"] = str(configured.get("backend") or "direct").strip().casefold() configured["layer_id"] = layer_id configured["layer"] = layer_id configured["lock_mode"] = configured["mode"] if configured["backend"] not in SUPPORTED_BACKENDS: return None, {"status": "invalid_config", "message": "repository backend must be direct or karman_bridge."} if configured["lock_mode"] not in SUPPORTED_LOCK_MODES: return None, {"status": "invalid_config", "message": "repository lock_mode must be automatic or manual."} runner = configured.get("runner") if isinstance(configured.get("runner"), dict) else {} configured["runner"] = runner runner_kind = str(runner.get("kind") or "local").strip().casefold() if runner_kind not in {"local", "http"}: return None, {"status": "invalid_config", "message": "repository runner.kind must be local or http."} runner["kind"] = runner_kind required = ("endpoint", "designer_path") if runner_kind == "local" and configured["lock_mode"] == "automatic" else () for key in required: if not str(configured.get(key) or "").strip(): return None, {"status": "invalid_config", "message": f"Repository configuration requires {key}."} if runner_kind == "http" and configured["lock_mode"] == "automatic" and not str(runner.get("url") or "").strip(): return None, {"status": "invalid_config", "message": "repository runner.url is required for runner.kind=http."} infobase = configured.get("infobase") if runner_kind == "local" and configured["lock_mode"] == "automatic" and (not isinstance(infobase, dict) or sum(bool(str(infobase.get(key) or "").strip()) for key in ("file", "server", "name")) != 1): return None, {"status": "invalid_config", "message": "infobase must contain exactly one of file, server, or name for runner.kind=local."} return configured, None def manual_confirmation_required(config: dict[str, Any]) -> bool: """Whether SQL-only operation needs the user's exact Configurator capture confirmation.""" if config.get("mode") in {"none", "unknown"} or config.get("connection_state") == "not_configured": return False return config.get("connection_state") == "unavailable" or config.get("lock_mode") == "manual" or not external_1c_enabled() def manual_confirmation_next_call(base_id: str, request_id: str) -> dict[str, Any]: """Machine-readable continuation; clients must not infer confirmation keys.""" return { "method": METHOD_CONFIRM, "params": { "base_id": base_id, "request_id": request_id, "user_confirmed_locked": True, }, } def _secret(config: dict[str, Any], field: str) -> str: env_name = str(config.get(f"{field}_env") or "").strip() return os.environ.get(env_name, "") if env_name else "" def _public_config(config: dict[str, Any]) -> dict[str, Any]: return { "mode": config.get("mode") or config.get("lock_mode"), "layer_id": config.get("layer_id") or config.get("layer"), "backend": config.get("backend"), "layer": config.get("layer"), "lock_mode": config.get("lock_mode"), "connection_state": config.get("connection_state"), "adapter_access_mode": "sql_only" if not external_1c_enabled() else "sql_and_external_1c", "automatic_repository_operations_available": external_1c_enabled(), "endpoint": config.get("endpoint"), "bridge_id": config.get("bridge_id") if config.get("backend") == "karman_bridge" else None, "runtime_version": config.get("runtime_version"), "runner_kind": (config.get("runner") or {}).get("kind"), "runner_url": (config.get("runner") or {}).get("url"), "runner_token_env": (config.get("runner") or {}).get("token_env"), "repository_user": str(config.get("repository_user") or ""), "repository_password_env": str(config.get("repository_password_env") or ""), "repository_user_configured": bool(str(config.get("repository_user") or "").strip()), "repository_password_configured": bool(_secret(config, "repository_password")), "infobase_user": str(config.get("infobase_user") or ""), "infobase_password_env": str(config.get("infobase_password_env") or ""), "infobase_user_configured": bool(str(config.get("infobase_user") or "").strip()), "infobase_password_configured": bool(_secret(config, "infobase_password")), } def public_development_layers(base_id: str) -> dict[str, Any]: base_item, error = _base_settings(base_id) if error or not isinstance(base_item, dict): return {} layers = base_item.get("development_layers") if isinstance(base_item.get("development_layers"), dict) else {} result: dict[str, Any] = {} for layer_id, layer in layers.items(): if not isinstance(layer, dict): continue repository, repository_error = repository_config(base_id, str(layer_id)) support = layer.get("support") if isinstance(layer.get("support"), dict) else {"mode": "unknown"} result[str(layer_id)] = { "repository": _public_config(repository) if repository and not repository_error else {"status": "invalid", "problem": repository_error}, "support": {"mode": str(support.get("mode") or "unknown"), "rules": dict(support.get("rules") or {}) if isinstance(support.get("rules"), dict) else {}}, } return result def _infobase_args(config: dict[str, Any]) -> list[str]: infobase = config["infobase"] if infobase.get("file"): args = ["/F", str(infobase["file"])] elif infobase.get("server"): args = ["/S", str(infobase["server"])] else: args = ["/IBName", str(infobase["name"])] user = str(config.get("infobase_user") or "").strip() if user: args += ["/N", user] password = _secret(config, "infobase_password") if password: args += ["/P", password] return args def _repository_args(config: dict[str, Any]) -> list[str]: args = ["/ConfigurationRepositoryF", str(config["endpoint"])] user = str(config.get("repository_user") or "").strip() if user: args += ["/ConfigurationRepositoryN", user] password = _secret(config, "repository_password") if password: args += ["/ConfigurationRepositoryP", password] extension = str(config.get("extension") or "").strip() if extension: args += ["-Extension", extension] return args def _safe_excerpt(value: str, config: dict[str, Any], limit: int = 4000) -> str: safe = value for secret in (_secret(config, "repository_password"), _secret(config, "infobase_password")): if secret: safe = safe.replace(secret, "[REDACTED]") return safe[-limit:] def _run_designer(config: dict[str, Any], operation: list[str], timeout_seconds: int) -> dict[str, Any]: started = time.monotonic() with tempfile.TemporaryDirectory(prefix="onec-repository-") as directory: log_path = Path(directory) / "designer.log" args = [str(config["designer_path"]), "DESIGNER"] args += _infobase_args(config) args += ["/DisableStartupMessages", "/DisableStartupDialogs", "/Out", str(log_path)] args += _repository_args(config) args += operation try: completed = subprocess.run(args, capture_output=True, text=True, timeout=timeout_seconds, check=False) except subprocess.TimeoutExpired as exc: return { "status": "timeout", "exit_code": None, "duration_ms": round((time.monotonic() - started) * 1000), "output": _safe_excerpt(str(exc.stdout or "") + str(exc.stderr or ""), config), } except OSError as exc: return {"status": "runner_error", "exit_code": None, "message": str(exc), "duration_ms": round((time.monotonic() - started) * 1000)} log = "" try: log = log_path.read_text(encoding="utf-8-sig", errors="replace") except OSError: pass output = "\n".join(part for part in (completed.stdout, completed.stderr, log) if part) return { "status": "ok" if completed.returncode == 0 else "failed", "exit_code": completed.returncode, "duration_ms": round((time.monotonic() - started) * 1000), "output": _safe_excerpt(output, config), } def _execute_repository( base_id: str, config: dict[str, Any], action: str, timeout_seconds: int, *, objects: list[str] | None = None, comment: str = "", keep_locked: bool = False, ) -> dict[str, Any]: if not external_1c_enabled(): return { "status": "external_1c_disabled", "message": "This adapter version is SQL-only. Use repository.lock.plan and the manual confirmation workflow.", } runner = config.get("runner") if isinstance(config.get("runner"), dict) else {"kind": "local"} if runner.get("kind") == "http": url = str(runner.get("url") or "").rstrip("/") + "/repository/execute" body = json.dumps( {"base_id": base_id, "action": action, "objects": objects or [], "comment": comment, "keep_locked": keep_locked}, ensure_ascii=False, ).encode("utf-8") headers = {"Content-Type": "application/json", "Accept": "application/json"} token_env = str(runner.get("token_env") or "").strip() token = os.environ.get(token_env, "") if token_env else "" if token: headers["Authorization"] = f"Bearer {token}" try: with urllib.request.urlopen(urllib.request.Request(url, data=body, headers=headers, method="POST"), timeout=timeout_seconds) as response: result = json.loads(response.read().decode("utf-8")) except urllib.error.HTTPError as exc: try: result = json.loads(exc.read().decode("utf-8")) except (OSError, json.JSONDecodeError, UnicodeDecodeError): result = {"status": "runner_error", "message": f"Repository runner returned HTTP {exc.code}."} return result if isinstance(result, dict) else {"status": "runner_error", "message": f"Repository runner returned HTTP {exc.code}."} except (OSError, urllib.error.URLError, json.JSONDecodeError) as exc: return {"status": "runner_error", "message": str(exc)} return result if isinstance(result, dict) else {"status": "runner_error", "message": "Repository runner returned a non-object response."} if action == "report": with tempfile.TemporaryDirectory(prefix="onec-repository-report-") as directory: report = Path(directory) / "report.txt" return _run_designer(config, ["/ConfigurationRepositoryReport", str(report), "-NBegin", "-1", "-ReportFormat", "txt"], timeout_seconds) with tempfile.TemporaryDirectory(prefix="onec-repository-objects-") as directory: objects_path = Path(directory) / "objects.txt" objects_path.write_text("\n".join(objects or []) + "\n", encoding="utf-8") if action == "lock": operation = ["/ConfigurationRepositoryLock", "-Objects", str(objects_path)] elif action == "unlock": operation = ["/ConfigurationRepositoryUnlock", "-Objects", str(objects_path)] elif action == "commit": operation = ["/ConfigurationRepositoryCommit", "-Objects", str(objects_path), "-Comment", comment] if keep_locked: operation.append("-KeepLocked") else: return {"status": "runner_error", "message": f"Unsupported repository action: {action}"} return _run_designer(config, operation, timeout_seconds) _CHILD_MARKERS = re.compile( r"\.(?:Реквизит|Attribute|ТабличнаяЧасть|TabularSection|Измерение|Dimension|Ресурс|Resource)\.", re.IGNORECASE, ) def development_object(ref: str) -> str: value = ref.strip().strip(".") match = _CHILD_MARKERS.search(value) if match: return value[: match.start()] for marker in (".МодульОбъекта", ".ObjectModule", ".МодульМенеджера", ".ManagerModule"): if value.casefold().endswith(marker.casefold()): return value[: -len(marker)] return value def _requested_objects(payload: dict[str, Any]) -> tuple[list[str] | None, dict[str, Any] | None]: raw = payload.get("objects") if raw is None: raw = [payload.get("object") or payload.get("ref") or payload.get("path")] if not isinstance(raw, list) or not raw: return None, {"status": "invalid_argument", "argument": "objects", "message": "Pass object/ref/path or a non-empty objects array."} values: list[str] = [] for index, item in enumerate(raw): if not isinstance(item, str) or not item.strip(): return None, {"status": "invalid_argument", "argument": f"objects[{index}]", "message": "Repository object must be a non-empty public 1C reference."} resolved = development_object(item) if resolved not in values: values.append(resolved) return values, None def lock_plan(payload: dict[str, Any]) -> dict[str, Any]: objects, error = _requested_objects(payload) if error: return {"schema": "onec_repository_lock_plan.v1", "method": METHOD_LOCK_PLAN, **error} operation = str(payload.get("operation") or "modify").strip().casefold() warnings: list[dict[str, str]] = [] if operation in {"add", "delete", "rename"}: warnings.append({"code": "parent_scope_requires_confirmation", "message": "Structural operations can require the parent/root and referenced objects; confirm the complete set before lock/apply."}) result = { "schema": "onec_repository_lock_plan.v1", "method": METHOD_LOCK_PLAN, "status": "ready" if not warnings else "needs_confirmation", "scope_status": "resolved", "operation": operation, "requested_objects": [str(x) for x in (payload.get("objects") or [payload.get("object") or payload.get("ref") or payload.get("path")])], "lock_objects": objects, "warnings": warnings, "native_lock_state": "unknown", "native_lock_verification": "not_available_via_infobase_sql" if not external_1c_enabled() else "not_checked", "capture_required": None, "sql_resolution": payload.get("sql_resolution") if isinstance(payload.get("sql_resolution"), list) else [], } base_id = str(payload.get("base_id") or "").strip() if base_id: layer_id = development_layer_id(payload) result["layer_id"] = layer_id config, config_error = repository_config(base_id, layer_id) if config_error: result["repository_problem"] = config_error elif config.get("mode") == "unknown" or config.get("connection_state") == "unknown": result["workflow"] = "blocked" result["status"] = "blocked_repository_connection_unknown" result["next_method"] = None elif config.get("mode") == "none" or config.get("connection_state") == "not_configured": result["workflow"] = "not_required" result["next_method"] = None elif manual_confirmation_required(config): result["workflow"] = "manual" result["next_method"] = METHOD_LOCK_REQUEST if not external_1c_enabled() else METHOD_CONFIRM result["user_action"] = { "action": "confirm_lock_in_configurator", "base_id": base_id, "objects": objects, "message": "SQL информационной базы не показывает нативное состояние захвата. Убедитесь в Конфигураторе, что перечисленные объекты уже захвачены указанным пользователем хранилища, и явно подтвердите тот же список через repository.lock.confirm.", } else: result["workflow"] = "automatic" result["next_method"] = METHOD_LOCK return result def create_lock_request(payload: dict[str, Any]) -> dict[str, Any]: base_id = str(payload.get("base_id") or "").strip() layer_id = development_layer_id(payload) config, error = repository_config(base_id, layer_id) if error: return {"schema": "onec_repository_lock_request.v1", "method": METHOD_LOCK_REQUEST, "base_id": base_id, **error} plan = lock_plan(payload) if plan.get("workflow") == "not_required": return {"schema": "onec_repository_lock_request.v1", "method": METHOD_LOCK_REQUEST, "base_id": base_id, "layer_id": layer_id, "status": "not_required", "plan": plan} if plan.get("status") == "needs_confirmation" and payload.get("confirm_repository_scope") is True: plan["status"] = "ready" if plan.get("status") != "ready": return {"schema": "onec_repository_lock_request.v1", "method": METHOD_LOCK_REQUEST, "base_id": base_id, "status": "blocked", "plan": plan} configured_repository_user = str(config.get("repository_user") or "").strip() requested_repository_user = str(payload.get("confirmed_repository_user") or payload.get("repository_user") or "").strip() if requested_repository_user and configured_repository_user and requested_repository_user.casefold() != configured_repository_user.casefold(): return { "schema": "onec_repository_lock_request.v1", "method": METHOD_LOCK_REQUEST, "base_id": base_id, "layer_id": layer_id, "status": "blocked", "error": "repository_user_mismatch", "expected_repository_user": configured_repository_user, } repository_user = requested_repository_user or configured_repository_user request_id = "rreq-" + uuid.uuid4().hex with _STATE_LOCK: state = _read_state() state.setdefault("requests", {})[request_id] = { "base_id": base_id, "layer": layer_id, "layer_id": layer_id, "backend": config.get("backend"), "operation": plan.get("operation"), "objects": plan["lock_objects"], "created_at": time.time(), "status": "pending_user_lock", "execution": "manual", "repository_user": repository_user, "sql_resolution": payload.get("sql_resolution") if isinstance(payload.get("sql_resolution"), list) else [], } _audit(state, "lock_request_created", request_id=request_id, base_id=base_id, objects=plan["lock_objects"]) _write_state(state) return { "schema": "onec_repository_lock_request.v1", "method": METHOD_LOCK_REQUEST, "base_id": base_id, "layer_id": layer_id, "status": "pending_user_lock", "request_id": request_id, "objects": plan["lock_objects"], "automatically_locked": False, "sql_resolution": payload.get("sql_resolution") if isinstance(payload.get("sql_resolution"), list) else [], "native_lock_state": "unknown", "native_lock_verification": "not_available_via_infobase_sql", "capture_required": None, "repository_user": repository_user or None, "repository_user_source": "layer_connection_setting" if configured_repository_user and not requested_repository_user else ("request" if requested_repository_user else "not_configured"), "user_action": "Убедитесь в Конфигураторе, что перечисленные объекты захвачены вами под настроенным пользователем хранилища, и явно подтвердите захват через repository.lock.confirm.", "next_method": METHOD_CONFIRM, "next_call": manual_confirmation_next_call(base_id, request_id), } def lock_request_status(payload: dict[str, Any]) -> dict[str, Any]: request_id = str(payload.get("request_id") or "").strip() request = (_read_state().get("requests") or {}).get(request_id) if not isinstance(request, dict): return {"schema": "onec_repository_lock_request_status.v1", "method": METHOD_LOCK_REQUEST_STATUS, "status": "not_found", "request_id": request_id} result = {"schema": "onec_repository_lock_request_status.v1", "method": METHOD_LOCK_REQUEST_STATUS, "status": request.get("status"), "request_id": request_id, "request": request} if request.get("status") == "pending_user_lock": result["next_method"] = METHOD_CONFIRM result["next_call"] = manual_confirmation_next_call(str(request.get("base_id") or ""), request_id) return result def cancel_lock_request(payload: dict[str, Any]) -> dict[str, Any]: request_id = str(payload.get("request_id") or "").strip() if payload.get("confirm_cancel") is not True: return {"schema": "onec_repository_lock_request_cancel.v1", "method": METHOD_LOCK_REQUEST_CANCEL, "status": "confirmation_required", "request_id": request_id} with _STATE_LOCK: state = _read_state() request = (state.get("requests") or {}).get(request_id) if not isinstance(request, dict): return {"schema": "onec_repository_lock_request_cancel.v1", "method": METHOD_LOCK_REQUEST_CANCEL, "status": "not_found", "request_id": request_id} if request.get("status") != "pending_user_lock": return {"schema": "onec_repository_lock_request_cancel.v1", "method": METHOD_LOCK_REQUEST_CANCEL, "status": "blocked", "error": "lock_request_not_pending", "request_id": request_id} request["status"] = "cancelled" request["cancelled_at"] = time.time() _audit(state, "lock_request_cancelled", request_id=request_id, base_id=request.get("base_id"), objects=request.get("objects")) _write_state(state) return {"schema": "onec_repository_lock_request_cancel.v1", "method": METHOD_LOCK_REQUEST_CANCEL, "status": "cancelled", "request_id": request_id} def _state_path() -> Path: return Path(os.environ.get("ONEC_REPOSITORY_STATE_FILE") or "/data/onec-repository-locks.json") def _read_state() -> dict[str, Any]: try: value = json.loads(_state_path().read_text(encoding="utf-8-sig")) state = value if isinstance(value, dict) else {"sessions": {}, "requests": {}, "audit": []} except (OSError, json.JSONDecodeError): state = {"sessions": {}, "requests": {}, "audit": []} now = time.time() request_ttl = max(60, int(os.environ.get("ONEC_REPOSITORY_REQUEST_TTL_SECONDS") or 86400)) session_ttl = max(60, int(os.environ.get("ONEC_REPOSITORY_CONFIRMATION_TTL_SECONDS") or 7200)) for request in (state.get("requests") or {}).values(): if isinstance(request, dict) and request.get("status") == "pending_user_lock" and now - float(request.get("created_at") if request.get("created_at") is not None else now) > request_ttl: request["status"] = "expired" request["expired_at"] = now for session in (state.get("sessions") or {}).values(): if isinstance(session, dict) and session.get("status") == "manual_confirmed" and now - float(session.get("created_at") if session.get("created_at") is not None else now) > session_ttl: session["status"] = "expired" session["expired_at"] = now return state def _write_state(value: dict[str, Any]) -> None: path = _state_path() path.parent.mkdir(parents=True, exist_ok=True) temporary = path.with_name(path.name + f".{uuid.uuid4().hex}.tmp") temporary.write_text(json.dumps(value, ensure_ascii=False, indent=2), encoding="utf-8") os.replace(temporary, path) def _audit(state: dict[str, Any], event: str, **details: Any) -> None: rows = state.setdefault("audit", []) rows.append({"event": event, "time": time.time(), **details}) if len(rows) > 5000: del rows[:-5000] def admin_state(base_id: str = "") -> dict[str, Any]: state = _read_state() requests = [{"request_id": key, **row} for key, row in (state.get("requests") or {}).items() if isinstance(row, dict) and (not base_id or row.get("base_id") == base_id)] sessions = [{"lock_session_id": key, **row} for key, row in (state.get("sessions") or {}).items() if isinstance(row, dict) and (not base_id or row.get("base_id") == base_id)] audit = [row for row in (state.get("audit") or []) if isinstance(row, dict) and (not base_id or row.get("base_id") == base_id)] requests.sort(key=lambda row: float(row.get("created_at") or 0), reverse=True) sessions.sort(key=lambda row: float(row.get("created_at") or 0), reverse=True) audit.sort(key=lambda row: float(row.get("time") or 0), reverse=True) return {"schema": "onec_repository_admin_state.v1", "base_id": base_id or None, "requests": requests, "sessions": sessions, "audit": audit[:200], "counts": {"requests": len(requests), "sessions": len(sessions), "audit": len(audit)}} def status(payload: dict[str, Any]) -> dict[str, Any]: base_id = str(payload.get("base_id") or "").strip() layer_id = development_layer_id(payload) config, error = repository_config(base_id, layer_id) if error: return {"schema": "onec_repository_status.v1", "method": METHOD_STATUS, "base_id": base_id, "connected": False, **error} result: dict[str, Any] = { "schema": "onec_repository_status.v1", "method": METHOD_STATUS, "base_id": base_id, "layer_id": layer_id, "status": "configured", "connected": True, "available": None, "repository": _public_config(config), "sql_lock_evidence": SQL_LOCK_EVIDENCE, } if config.get("mode") == "none" or config.get("connection_state") == "not_configured": result.update({"status": "repository_not_connected", "connected": False, "available": None}) return result if config.get("mode") == "unknown" or config.get("connection_state") == "unknown": result.update({"status": "repository_connection_unknown", "connected": False, "available": None}) return result if manual_confirmation_required(config): result.update({"status": "manual_confirmation_required", "connected": False, "available": False if config.get("connection_state") == "unavailable" else None}) if bool(payload.get("probe")): result["probe"] = {"status": "not_applicable", "message": "Manual SQL-only workflow does not establish a native repository connection."} return result if not bool(payload.get("probe")): return result if not external_1c_enabled(): result["status"] = "sql_only" result["connected"] = False result["available"] = None result["probe"] = {"status": "not_supported", "message": "External 1C access is disabled in this SQL-only adapter version."} return result timeout_seconds = int(payload.get("timeout_seconds") or 60) executed = _execute_repository(base_id, config, "report", timeout_seconds) result["probe"] = executed result["available"] = executed.get("status") == "ok" result["status"] = "ready" if result["available"] else "blocked_repository_unavailable" return result def lock(payload: dict[str, Any]) -> dict[str, Any]: base_id = str(payload.get("base_id") or "").strip() layer_id = development_layer_id(payload) config, error = repository_config(base_id, layer_id) if error: return {"schema": "onec_repository_lock.v1", "method": METHOD_LOCK, "base_id": base_id, **error} if manual_confirmation_required(config): return { "schema": "onec_repository_lock.v1", "method": METHOD_LOCK, "base_id": base_id, "status": "manual_action_required", "plan": lock_plan(payload), "next_method": METHOD_CONFIRM, } plan = lock_plan(payload) if plan.get("status") == "needs_confirmation" and payload.get("confirm_repository_scope") is True: plan["status"] = "ready" plan["scope_confirmed"] = True if plan.get("status") != "ready": return {"schema": "onec_repository_lock.v1", "method": METHOD_LOCK, "base_id": base_id, "status": "blocked", "plan": plan} if not payload.get("allow_repository_lock") is True: return {"schema": "onec_repository_lock.v1", "method": METHOD_LOCK, "base_id": base_id, "status": "blocked", "error": "explicit_repository_lock_required", "plan": plan} layer = str(config.get("layer") or "base") with _base_lock(base_id, layer): executed = _execute_repository(base_id, config, "lock", int(payload.get("timeout_seconds") or 120), objects=plan["lock_objects"]) if executed.get("status") != "ok": return {"schema": "onec_repository_lock.v1", "method": METHOD_LOCK, "base_id": base_id, "status": "blocked", "error": "repository_lock_failed", "plan": plan, "execution": executed} session_id = "rlock-" + uuid.uuid4().hex state = _read_state() sessions = state.setdefault("sessions", {}) sessions[session_id] = {"base_id": base_id, "layer": layer_id, "layer_id": layer_id, "backend": config.get("backend"), "objects": plan["lock_objects"], "created_at": time.time(), "status": "acquired"} _write_state(state) return {"schema": "onec_repository_lock.v1", "method": METHOD_LOCK, "base_id": base_id, "status": "acquired", "lock_session_id": session_id, "acquired": plan["lock_objects"], "backend": config.get("backend"), "execution": executed} def confirm_manual_lock(payload: dict[str, Any]) -> dict[str, Any]: base_id = str(payload.get("base_id") or "").strip() layer_id = development_layer_id(payload) config, error = repository_config(base_id, layer_id) if error: return {"schema": "onec_repository_manual_lock.v1", "method": METHOD_CONFIRM, "base_id": base_id, **error} if not manual_confirmation_required(config): return {"schema": "onec_repository_manual_lock.v1", "method": METHOD_CONFIRM, "base_id": base_id, "status": "blocked", "error": "manual_lock_mode_required"} if not external_1c_enabled() and not str(payload.get("request_id") or "").strip(): return {"schema": "onec_repository_manual_lock.v1", "method": METHOD_CONFIRM, "base_id": base_id, "status": "blocked", "error": "lock_request_required", "next_method": METHOD_LOCK_REQUEST} state = _read_state() request_id = str(payload.get("request_id") or "").strip() request = (state.get("requests") or {}).get(request_id) if request_id else None if request_id and (not isinstance(request, dict) or request.get("base_id") != base_id): return {"schema": "onec_repository_manual_lock.v1", "method": METHOD_CONFIRM, "base_id": base_id, "status": "blocked", "error": "lock_request_not_found", "request_id": request_id} if isinstance(request, dict) and request.get("status") != "pending_user_lock": return {"schema": "onec_repository_manual_lock.v1", "method": METHOD_CONFIRM, "base_id": base_id, "status": "blocked", "error": "lock_request_not_pending", "request_id": request_id} effective_payload = dict(payload) if isinstance(request, dict): layer_id = str(request.get("layer_id") or request.get("layer") or "base") config, error = repository_config(base_id, layer_id) if error: return {"schema": "onec_repository_manual_lock.v1", "method": METHOD_CONFIRM, "base_id": base_id, **error} if not manual_confirmation_required(config): return {"schema": "onec_repository_manual_lock.v1", "method": METHOD_CONFIRM, "base_id": base_id, "status": "blocked", "error": "manual_lock_mode_required"} effective_payload["objects"] = [str(item) for item in request.get("objects") or []] effective_payload["layer_id"] = layer_id effective_payload["sql_resolution"] = request.get("sql_resolution") if isinstance(request.get("sql_resolution"), list) else [] if payload.get("user_confirmed_locked") is not True: response: dict[str, Any] = { "schema": "onec_repository_manual_lock.v1", "method": METHOD_CONFIRM, "base_id": base_id, "layer_id": layer_id, "status": "confirmation_required", "error": "user_confirmed_locked_required", "required_fields": ["base_id", "request_id", "user_confirmed_locked"], "next_call": manual_confirmation_next_call(base_id, request_id), } if isinstance(request, dict): response.update({ "request_id": request_id, "objects": list(request.get("objects") or []), "repository_user": str(request.get("repository_user") or config.get("repository_user") or "") or None, "sql_resolution": effective_payload["sql_resolution"], }) return response plan = lock_plan(effective_payload) if plan.get("status") == "needs_confirmation" and payload.get("confirm_repository_scope") is True: plan["status"] = "ready" if plan.get("status") != "ready": return {"schema": "onec_repository_manual_lock.v1", "method": METHOD_CONFIRM, "base_id": base_id, "status": "blocked", "plan": plan} requested_repository_user = str(payload.get("confirmed_repository_user") or payload.get("repository_user") or "").strip() expected_repository_user = str(config.get("repository_user") or "").strip() request_repository_user = str(request.get("repository_user") or "").strip() if isinstance(request, dict) else "" confirmed_repository_user = requested_repository_user or request_repository_user or expected_repository_user if not confirmed_repository_user: return {"schema": "onec_repository_manual_lock.v1", "method": METHOD_CONFIRM, "base_id": base_id, "status": "confirmation_required", "error": "repository_user_confirmation_required", "plan": plan} if expected_repository_user and confirmed_repository_user.casefold() != expected_repository_user.casefold(): return {"schema": "onec_repository_manual_lock.v1", "method": METHOD_CONFIRM, "base_id": base_id, "status": "blocked", "error": "repository_user_mismatch", "expected_repository_user": expected_repository_user} session_id = "rlock-" + uuid.uuid4().hex state.setdefault("sessions", {})[session_id] = { "base_id": base_id, "layer": layer_id, "layer_id": layer_id, "backend": config.get("backend"), "objects": plan["lock_objects"], "created_at": time.time(), "status": "manual_confirmed", "verification": "user_confirmation_only", "automatically_verified": False, "repository_user": confirmed_repository_user, **({"request_id": request_id} if request_id else {}), } if isinstance(request, dict): request["status"] = "confirmed_by_user" request["confirmed_at"] = time.time() request["lock_session_id"] = session_id _audit(state, "manual_lock_confirmed", request_id=request_id or None, lock_session_id=session_id, base_id=base_id, objects=plan["lock_objects"], repository_user=confirmed_repository_user) _write_state(state) return { "schema": "onec_repository_manual_lock.v1", "method": METHOD_CONFIRM, "base_id": base_id, "status": "manual_confirmed", "layer_id": layer_id, "lock_session_id": session_id, "request_id": request_id or None, "objects": plan["lock_objects"], "capture_state": "manually_confirmed", "native_lock_state": "unknown", "native_lock_verification": "not_available_via_infobase_sql", "automatically_verified": False, "repository_user": confirmed_repository_user, "write_context": { "lock_session_id": session_id, "repository_object": plan["lock_objects"][0] if len(plan["lock_objects"]) == 1 else None, "layer_id": layer_id, "locked_objects": plan["lock_objects"], "instruction": "Copy lock_session_id into metadata.write.preflight and code.write. For one object, repository_object is supplied for clients that do not resolve module ownership themselves.", }, "warning": "Адаптер принял явное подтверждение пользователя, но не проверял захват через API хранилища.", } def verify(payload: dict[str, Any]) -> dict[str, Any]: session_id = str(payload.get("lock_session_id") or "").strip() session = (_read_state().get("sessions") or {}).get(session_id) if not isinstance(session, dict): return {"schema": "onec_repository_lock_verify.v1", "method": METHOD_VERIFY, "status": "not_found", "lock_session_id": session_id} verify_status = "owned_by_adapter" if session.get("status") == "acquired" else ("manual_confirmation_unverified" if session.get("status") == "manual_confirmed" else session.get("status")) return {"schema": "onec_repository_lock_verify.v1", "method": METHOD_VERIFY, "status": verify_status, "lock_session_id": session_id, "session": session} def close_manual_lock(payload: dict[str, Any]) -> dict[str, Any]: session_id = str(payload.get("lock_session_id") or "").strip() if payload.get("user_confirmed_released") is not True: return {"schema": "onec_repository_lock_close.v1", "method": METHOD_CLOSE, "status": "confirmation_required", "lock_session_id": session_id} with _STATE_LOCK: state = _read_state() session = (state.get("sessions") or {}).get(session_id) if not isinstance(session, dict): return {"schema": "onec_repository_lock_close.v1", "method": METHOD_CLOSE, "status": "not_found", "lock_session_id": session_id} if session.get("status") not in {"manual_confirmed", "closed"}: return {"schema": "onec_repository_lock_close.v1", "method": METHOD_CLOSE, "status": "blocked", "error": "manual_confirmation_not_active", "lock_session_id": session_id} already_closed = session.get("status") == "closed" session["status"] = "closed" closed_at = float(session.get("closed_at") or time.time()) session["closed_at"] = closed_at request_id = str(session.get("request_id") or "").strip() request = (state.get("requests") or {}).get(request_id) if request_id else None if not isinstance(request, dict): request_id = next( ( str(candidate_id) for candidate_id, candidate in (state.get("requests") or {}).items() if isinstance(candidate, dict) and candidate.get("lock_session_id") == session_id ), "", ) request = (state.get("requests") or {}).get(request_id) if request_id else None if isinstance(request, dict): request["status"] = "closed" request["closed_at"] = closed_at if not already_closed: _audit(state, "manual_lock_closed", request_id=request_id or None, lock_session_id=session_id, base_id=session.get("base_id"), objects=session.get("objects")) _write_state(state) return { "schema": "onec_repository_lock_close.v1", "method": METHOD_CLOSE, "status": "closed", "lock_session_id": session_id, "request_id": request_id or None, } def write_gate(payload: dict[str, Any]) -> dict[str, Any]: base_id = str(payload.get("base_id") or "").strip() layer_id = development_layer_id(payload) config, error = repository_config(base_id, layer_id) if error and error.get("status") == "not_configured": return {"required": False, "allowed": True, "status": "not_configured", "layer_id": layer_id} if error: return {"required": True, "allowed": False, "status": "blocked_repository_configuration", "layer_id": layer_id, "problem": error} if config.get("mode") == "none" or config.get("connection_state") == "not_configured": return {"required": False, "allowed": True, "status": "repository_not_connected", "layer_id": layer_id} if config.get("mode") == "unknown" or config.get("connection_state") == "unknown": return {"required": True, "allowed": False, "status": "blocked_repository_connection_unknown", "layer_id": layer_id} session_id = str(payload.get("lock_session_id") or "").strip() if not session_id: return { "required": True, "allowed": False, "status": "needs_repository_lock", "backend": config.get("backend"), "layer_id": layer_id, "next_method": METHOD_LOCK_PLAN, } session = (_read_state().get("sessions") or {}).get(session_id) if not isinstance(session, dict): return {"required": True, "allowed": False, "status": "blocked_repository_lock_session", "error": "lock_session_not_found", "layer_id": layer_id, "lock_session_id": session_id} if session.get("base_id") != base_id: return {"required": True, "allowed": False, "status": "blocked_repository_lock_session", "error": "lock_session_base_mismatch", "layer_id": layer_id, "lock_session_id": session_id, "session_base_id": session.get("base_id")} session_layer_id = str(session.get("layer_id") or session.get("layer") or "base") if session_layer_id != layer_id: return {"required": True, "allowed": False, "status": "blocked_repository_lock_session", "error": "lock_layer_mismatch", "layer_id": layer_id, "session_layer_id": session_layer_id, "lock_session_id": session_id} if session.get("status") not in {"acquired", "manual_confirmed"}: return {"required": True, "allowed": False, "status": "blocked_repository_lock_session", "error": "lock_session_inactive", "layer_id": layer_id, "lock_session_id": session_id, "session_status": session.get("status")} target = payload.get("target") if isinstance(payload.get("target"), dict) else {} requested = "" for value in ( payload.get("repository_object"), target.get("repository_object"), target.get("canonical_path"), payload.get("canonical_path"), target.get("path"), payload.get("path"), payload.get("ref"), payload.get("object"), ): if isinstance(value, str) and value.strip(): requested = development_object(value) break if not requested: kind = str(payload.get("object_type") or payload.get("kind") or target.get("object_type") or target.get("kind") or "").strip() name = str(payload.get("object_name") or payload.get("name") or target.get("object_name") or target.get("name") or "").strip() if kind and name: requested = development_object(f"{kind}.{name}") if not requested: return { "required": True, "allowed": False, "status": "blocked_repository_scope_unresolved", "lock_session_id": session_id, "message": "Pass repository_object with the public 1C development-object reference for this low-level write route.", } locked = [str(item) for item in session.get("objects") or []] if requested.casefold() not in {item.casefold() for item in locked}: return { "required": True, "allowed": False, "status": "blocked_repository_scope_mismatch", "error": "lock_object_mismatch", "lock_session_id": session_id, "requested_object": requested, "locked_objects": locked, } return { "required": True, "allowed": True, "status": "ready", "backend": config.get("backend"), "layer_id": layer_id, "lock_session_id": session_id, "requested_object": requested, "objects": locked, "verification": "automatic" if session.get("status") == "acquired" else "user_confirmation_only", } def support_gate(payload: dict[str, Any]) -> dict[str, Any]: """Evaluate the independently configured support policy for one exact target. Repository scope may collapse a child to its development owner. Support scope intentionally does not: a form or another child can have its own support rule. """ base_id = str(payload.get("base_id") or "").strip() layer_id = development_layer_id(payload) base_item, base_error = _base_settings(base_id) if base_error: if base_error.get("status") == "not_configured": return {"required": False, "allowed": True, "status": "support_not_configured_legacy", "layer_id": layer_id, "source": "legacy_configuration"} return {"required": True, "allowed": False, "status": "blocked_support_configuration", "layer_id": layer_id, "problem": base_error} layers = base_item.get("development_layers") if isinstance(base_item.get("development_layers"), dict) else None if layers is None: return {"required": False, "allowed": True, "status": "support_not_configured_legacy", "layer_id": layer_id, "source": "legacy_configuration"} layer = layers.get(layer_id) if not isinstance(layer, dict): return {"required": True, "allowed": False, "status": "blocked_support_layer_unknown", "layer_id": layer_id} support = layer.get("support") if not isinstance(support, dict): return {"required": True, "allowed": False, "status": "blocked_support_unknown", "layer_id": layer_id, "source": "development_layer_configuration"} mode = str(support.get("mode") or "unknown").strip().casefold() if mode not in SUPPORTED_SUPPORT_MODES: return {"required": True, "allowed": False, "status": "blocked_support_configuration", "layer_id": layer_id, "problem": {"message": "support.mode must be none, editable, locked, rules, or unknown."}} if mode == "none": return {"required": False, "allowed": True, "status": "not_on_support", "layer_id": layer_id, "mode": mode} if mode == "editable": return {"required": True, "allowed": True, "status": "support_editable", "layer_id": layer_id, "mode": mode} if mode == "locked": return {"required": True, "allowed": False, "status": "blocked_by_support", "layer_id": layer_id, "mode": mode} target = payload.get("target") if isinstance(payload.get("target"), dict) else {} scope = str(target.get("support_scope") or target.get("canonical_path") or target.get("path") or payload.get("support_scope") or payload.get("canonical_path") or payload.get("path") or payload.get("ref") or payload.get("object") or "").strip() if mode == "rules" and scope: rules = support.get("rules") if isinstance(support.get("rules"), dict) else {} candidates = [] folded = scope.casefold() for rule_scope, rule_value in rules.items(): key = str(rule_scope).strip() if key and (folded == key.casefold() or folded.startswith(key.casefold() + ".")): candidates.append((len(key), key, str(rule_value or "unknown").strip().casefold())) if candidates: _, matched_scope, rule = max(candidates) allowed = rule in {"editable", "vendor_editable_support_preserved", "not_supported"} return { "required": True, "allowed": allowed, "status": "support_rule_editable" if allowed else ("blocked_by_support_rule" if rule in {"locked", "vendor_not_editable"} else "blocked_support_unknown"), "layer_id": layer_id, "mode": mode, "scope": scope, "matched_scope": matched_scope, "rule": rule, } return {"required": True, "allowed": False, "status": "blocked_support_unknown", "layer_id": layer_id, "mode": mode, "scope": scope or None} def commit_plan(payload: dict[str, Any]) -> dict[str, Any]: session_id = str(payload.get("lock_session_id") or "").strip() session = (_read_state().get("sessions") or {}).get(session_id) if not isinstance(session, dict): return {"schema": "onec_repository_commit_plan.v1", "method": METHOD_COMMIT_PLAN, "status": "not_found", "lock_session_id": session_id} comment = str(payload.get("comment") or "").strip() problems: list[dict[str, str]] = [] if session.get("status") != "acquired": problems.append({"code": "lock_session_not_acquired", "message": "Commit requires an active adapter lock session."}) if not comment: problems.append({"code": "commit_comment_required", "message": "A non-empty repository version comment is required."}) return { "schema": "onec_repository_commit_plan.v1", "method": METHOD_COMMIT_PLAN, "status": "ready" if not problems else "blocked", "allowed": not problems, "lock_session_id": session_id, "base_id": session.get("base_id"), "objects": session.get("objects") or [], "comment": comment, "problems": problems, } def commit(payload: dict[str, Any]) -> dict[str, Any]: plan = commit_plan(payload) session_id = str(payload.get("lock_session_id") or "").strip() if not plan.get("allowed"): return {"schema": "onec_repository_commit.v1", "method": METHOD_COMMIT, "status": "blocked", "lock_session_id": session_id, "plan": plan} if payload.get("allow_repository_commit") is not True: return {"schema": "onec_repository_commit.v1", "method": METHOD_COMMIT, "status": "blocked", "error": "explicit_repository_commit_required", "lock_session_id": session_id, "plan": plan} state = _read_state() session = (state.get("sessions") or {}).get(session_id) base_id = str(session.get("base_id") or "") config, error = repository_config(base_id, str(session.get("layer_id") or session.get("layer") or "base")) if error: return {"schema": "onec_repository_commit.v1", "method": METHOD_COMMIT, "lock_session_id": session_id, **error} with _base_lock(base_id, str(session.get("layer") or "base")): executed = _execute_repository(base_id, config, "commit", int(payload.get("timeout_seconds") or 180), objects=[str(item) for item in session.get("objects") or []], comment=str(plan["comment"]), keep_locked=payload.get("keep_locked") is True) if executed.get("status") != "ok": return {"schema": "onec_repository_commit.v1", "method": METHOD_COMMIT, "status": "blocked", "error": "repository_commit_failed", "lock_session_id": session_id, "execution": executed} session["status"] = "acquired" if payload.get("keep_locked") is True else "committed" session["committed_at"] = time.time() session["commit_comment"] = str(plan["comment"]) _write_state(state) return {"schema": "onec_repository_commit.v1", "method": METHOD_COMMIT, "status": session["status"], "lock_session_id": session_id, "committed": session.get("objects"), "keep_locked": payload.get("keep_locked") is True, "execution": executed} def unlock(payload: dict[str, Any]) -> dict[str, Any]: session_id = str(payload.get("lock_session_id") or "").strip() state = _read_state() session = (state.get("sessions") or {}).get(session_id) if not isinstance(session, dict): return {"schema": "onec_repository_unlock.v1", "method": METHOD_UNLOCK, "status": "not_found", "lock_session_id": session_id} if session.get("status") != "acquired": return {"schema": "onec_repository_unlock.v1", "method": METHOD_UNLOCK, "status": str(session.get("status")), "lock_session_id": session_id} if not payload.get("allow_repository_unlock") is True: return {"schema": "onec_repository_unlock.v1", "method": METHOD_UNLOCK, "status": "blocked", "error": "explicit_repository_unlock_required", "lock_session_id": session_id} base_id = str(session.get("base_id") or "") config, error = repository_config(base_id, str(session.get("layer_id") or session.get("layer") or "base")) if error: return {"schema": "onec_repository_unlock.v1", "method": METHOD_UNLOCK, "lock_session_id": session_id, **error} with _base_lock(base_id, str(session.get("layer") or "base")): executed = _execute_repository(base_id, config, "unlock", int(payload.get("timeout_seconds") or 120), objects=[str(item) for item in session.get("objects") or []]) if executed.get("status") != "ok": return {"schema": "onec_repository_unlock.v1", "method": METHOD_UNLOCK, "status": "blocked", "error": "repository_unlock_failed", "lock_session_id": session_id, "execution": executed} session["status"] = "released" session["released_at"] = time.time() _write_state(state) return {"schema": "onec_repository_unlock.v1", "method": METHOD_UNLOCK, "status": "released", "lock_session_id": session_id, "released": session.get("objects"), "execution": executed} def call(method: str, payload: dict[str, Any]) -> dict[str, Any]: if method == METHOD_STATUS: return status(payload) if method == METHOD_LOCK_PLAN: return lock_plan(payload) if method == METHOD_LOCK_REQUEST: return create_lock_request(payload) if method == METHOD_LOCK_REQUEST_STATUS: return lock_request_status(payload) if method == METHOD_LOCK_REQUEST_CANCEL: return cancel_lock_request(payload) if method == METHOD_LOCK: return lock(payload) if method == METHOD_CONFIRM: return confirm_manual_lock(payload) if method == METHOD_VERIFY: return verify(payload) if method == METHOD_CLOSE: return close_manual_lock(payload) if method == METHOD_UNLOCK: return unlock(payload) if method == METHOD_COMMIT_PLAN: return commit_plan(payload) if method == METHOD_COMMIT: return commit(payload) return {"status": "method_not_found", "method": method}