Complete name-first 1C adapter saved-state support
This commit is contained in:
@@ -15,6 +15,14 @@ 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"
|
||||
@@ -29,6 +37,9 @@ 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()
|
||||
@@ -66,13 +77,60 @@ def _load_json_map(env_name: str, file_env_name: str) -> tuple[dict[str, Any] |
|
||||
return value, None
|
||||
|
||||
|
||||
def repository_config(base_id: str) -> tuple[dict[str, Any] | None, dict[str, Any] | 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
|
||||
item = base_item.get("repository") if isinstance(base_item, 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 error:
|
||||
if repository_error and layer_error:
|
||||
return None, repository_error
|
||||
item = values.get(base_id) if values else None
|
||||
if item is None:
|
||||
@@ -80,9 +138,21 @@ def repository_config(base_id: str) -> tuple[dict[str, Any] | None, dict[str, An
|
||||
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"] = str(configured.get("layer") or "base").strip().casefold()
|
||||
configured["lock_mode"] = str(configured.get("lock_mode") or "automatic").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:
|
||||
@@ -105,6 +175,25 @@ def repository_config(base_id: str) -> tuple[dict[str, Any] | None, dict[str, An
|
||||
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 ""
|
||||
@@ -112,9 +201,12 @@ def _secret(config: dict[str, Any], field: str) -> str:
|
||||
|
||||
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"),
|
||||
@@ -134,6 +226,24 @@ def _public_config(config: dict[str, Any]) -> dict[str, Any]:
|
||||
}
|
||||
|
||||
|
||||
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"):
|
||||
@@ -311,24 +421,38 @@ def lock_plan(payload: dict[str, Any]) -> dict[str, Any]:
|
||||
"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:
|
||||
config, config_error = repository_config(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("lock_mode") == "manual":
|
||||
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_CONFIRM
|
||||
result["next_method"] = METHOD_LOCK_REQUEST if not external_1c_enabled() else METHOD_CONFIRM
|
||||
result["user_action"] = {
|
||||
"action": "lock_in_configurator",
|
||||
"action": "confirm_lock_in_configurator",
|
||||
"base_id": base_id,
|
||||
"objects": objects,
|
||||
"message": "Захватите перечисленные объекты в Конфигураторе, затем явно подтвердите тот же список через repository.lock.confirm.",
|
||||
"message": "SQL информационной базы не показывает нативное состояние захвата. Убедитесь в Конфигураторе, что перечисленные объекты уже захвачены указанным пользователем хранилища, и явно подтвердите тот же список через repository.lock.confirm.",
|
||||
}
|
||||
else:
|
||||
result["workflow"] = "automatic"
|
||||
@@ -338,36 +462,57 @@ def lock_plan(payload: dict[str, Any]) -> dict[str, Any]:
|
||||
|
||||
def create_lock_request(payload: dict[str, Any]) -> dict[str, Any]:
|
||||
base_id = str(payload.get("base_id") or "").strip()
|
||||
config, error = repository_config(base_id)
|
||||
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": str(config.get("layer") or "base"),
|
||||
"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, "status": "pending_user_lock", "request_id": request_id,
|
||||
"base_id": base_id, "layer_id": layer_id, "status": "pending_user_lock", "request_id": request_id,
|
||||
"objects": plan["lock_objects"], "automatically_locked": False,
|
||||
"user_action": "Захватите перечисленные объекты в Конфигураторе и подтвердите заявку через repository.lock.confirm.",
|
||||
"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),
|
||||
}
|
||||
|
||||
|
||||
@@ -376,7 +521,11 @@ def lock_request_status(payload: dict[str, Any]) -> dict[str, Any]:
|
||||
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}
|
||||
return {"schema": "onec_repository_lock_request_status.v1", "method": METHOD_LOCK_REQUEST_STATUS, "status": request.get("status"), "request_id": request_id, "request": request}
|
||||
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]:
|
||||
@@ -449,25 +598,34 @@ def admin_state(base_id: str = "") -> dict[str, Any]:
|
||||
|
||||
def status(payload: dict[str, Any]) -> dict[str, Any]:
|
||||
base_id = str(payload.get("base_id") or "").strip()
|
||||
config, error = repository_config(base_id)
|
||||
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,
|
||||
"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
|
||||
if config.get("lock_mode") == "manual":
|
||||
result["status"] = "manual_workflow"
|
||||
result["available"] = None
|
||||
result["probe"] = {"status": "not_applicable", "message": "Manual lock mode does not require Designer or a repository runner. Use repository.lock.plan."}
|
||||
return result
|
||||
timeout_seconds = int(payload.get("timeout_seconds") or 60)
|
||||
executed = _execute_repository(base_id, config, "report", timeout_seconds)
|
||||
result["probe"] = executed
|
||||
@@ -478,10 +636,11 @@ def status(payload: dict[str, Any]) -> dict[str, Any]:
|
||||
|
||||
def lock(payload: dict[str, Any]) -> dict[str, Any]:
|
||||
base_id = str(payload.get("base_id") or "").strip()
|
||||
config, error = repository_config(base_id)
|
||||
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 config.get("lock_mode") == "manual":
|
||||
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,
|
||||
@@ -502,17 +661,18 @@ def lock(payload: dict[str, Any]) -> dict[str, Any]:
|
||||
session_id = "rlock-" + uuid.uuid4().hex
|
||||
state = _read_state()
|
||||
sessions = state.setdefault("sessions", {})
|
||||
sessions[session_id] = {"base_id": base_id, "layer": layer, "backend": config.get("backend"), "objects": plan["lock_objects"], "created_at": time.time(), "status": "acquired"}
|
||||
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()
|
||||
config, error = repository_config(base_id)
|
||||
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 config.get("lock_mode") != "manual":
|
||||
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}
|
||||
@@ -525,31 +685,73 @@ def confirm_manual_lock(payload: dict[str, Any]) -> dict[str, Any]:
|
||||
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}
|
||||
if payload.get("user_confirmed_locked") is not True:
|
||||
return {"schema": "onec_repository_manual_lock.v1", "method": METHOD_CONFIRM, "base_id": base_id, "status": "confirmation_required", "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": str(config.get("layer") or "base"), "backend": config.get("backend"),
|
||||
"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"])
|
||||
_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", "lock_session_id": session_id, "request_id": request_id or None, "objects": plan["lock_objects"],
|
||||
"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 хранилища.",
|
||||
}
|
||||
|
||||
@@ -604,20 +806,32 @@ def close_manual_lock(payload: dict[str, Any]) -> dict[str, Any]:
|
||||
|
||||
def write_gate(payload: dict[str, Any]) -> dict[str, Any]:
|
||||
base_id = str(payload.get("base_id") or "").strip()
|
||||
config, error = repository_config(base_id)
|
||||
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"}
|
||||
return {"required": False, "allowed": True, "status": "not_configured", "layer_id": layer_id}
|
||||
if error:
|
||||
return {"required": True, "allowed": False, "status": "blocked_repository_configuration", "problem": 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"), "next_method": METHOD_LOCK_PLAN,
|
||||
"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) or session.get("base_id") != base_id or session.get("status") not in {"acquired", "manual_confirmed"}:
|
||||
return {"required": True, "allowed": False, "status": "blocked_repository_lock_session", "lock_session_id": 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 (
|
||||
@@ -641,15 +855,67 @@ def write_gate(payload: dict[str, Any]) -> dict[str, Any]:
|
||||
if requested.casefold() not in {item.casefold() for item in locked}:
|
||||
return {
|
||||
"required": True, "allowed": False, "status": "blocked_repository_scope_mismatch",
|
||||
"lock_session_id": session_id, "requested_object": requested, "locked_objects": locked,
|
||||
"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"),
|
||||
"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)
|
||||
@@ -679,7 +945,7 @@ def commit(payload: dict[str, Any]) -> dict[str, Any]:
|
||||
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)
|
||||
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")):
|
||||
@@ -704,7 +970,7 @@ def unlock(payload: dict[str, Any]) -> dict[str, Any]:
|
||||
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)
|
||||
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")):
|
||||
|
||||
Reference in New Issue
Block a user