Initial project import

This commit is contained in:
2026-08-14 09:40:51 +03:00
parent 00040e5ce4
commit d7099bf80d
146 changed files with 30509 additions and 1055 deletions
+522 -47
View File
@@ -1,8 +1,10 @@
from __future__ import annotations
import json
import hashlib
import os
import re
import sqlite3
import subprocess
import tempfile
import threading
@@ -10,6 +12,7 @@ import time
import urllib.error
import urllib.request
import uuid
from contextlib import contextmanager
from pathlib import Path
from typing import Any
@@ -43,6 +46,7 @@ SUPPORTED_SUPPORT_MODES = {"none", "editable", "locked", "rules", "unknown"}
_BASE_LOCKS: dict[str, threading.Lock] = {}
_BASE_LOCKS_GUARD = threading.Lock()
_STATE_LOCK = threading.RLock()
REPOSITORY_STATE_SCHEMA_VERSION = 2
def external_1c_enabled() -> bool:
@@ -124,7 +128,20 @@ def repository_config(base_id: str, layer_id: str = "base") -> tuple[dict[str, A
else:
item = None
if isinstance(base_item, dict) and isinstance(base_item.get("development_layers"), dict):
return None, layer_error
# A disposable base can explicitly declare that its base layer has
# no repository at all. Extensions in such a base are not new
# repository layers merely because the adapter has discovered them
# after the configuration file was written. Inherit only this
# unambiguous no-repository fact; never inherit a manual/automatic
# repository policy to an extension.
base_layer = base_item["development_layers"].get("base")
base_repository = base_layer.get("repository") if isinstance(base_layer, dict) and isinstance(base_layer.get("repository"), dict) else None
base_mode = str((base_repository or {}).get("mode") or (base_repository or {}).get("lock_mode") or "").strip().casefold()
base_connection = str((base_repository or {}).get("connection_state") or "").strip().casefold()
if layer_id != "base" and (base_mode == "none" or base_connection == "not_configured"):
item = {"mode": "none", "connection_state": "not_configured", "inherited_from_layer": "base"}
else:
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")
@@ -148,7 +165,11 @@ def repository_config(base_id: str, layer_id: str = "base") -> tuple[dict[str, A
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
return {
"mode": "none", "lock_mode": "manual", "connection_state": configured["connection_state"],
"layer_id": layer_id, "layer": layer_id,
**({"inherited_from_layer": configured["inherited_from_layer"]} if configured.get("inherited_from_layer") else {}),
}, None
configured["backend"] = str(configured.get("backend") or "direct").strip().casefold()
configured["layer_id"] = layer_id
configured["layer"] = layer_id
@@ -207,6 +228,7 @@ def _public_config(config: dict[str, Any]) -> dict[str, Any]:
"layer": config.get("layer"),
"lock_mode": config.get("lock_mode"),
"connection_state": config.get("connection_state"),
"inherited_from_layer": config.get("inherited_from_layer"),
"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"),
@@ -317,6 +339,119 @@ def _run_designer(config: dict[str, Any], operation: list[str], timeout_seconds:
}
def activation_debug_probe(
base_id: str,
config: dict[str, Any],
*,
layer: str,
timeout_seconds: int,
request_id: str = "",
fingerprint: str = "",
) -> dict[str, Any]:
"""Probe runner readiness without starting Designer or reading configured credentials."""
runner = config.get("runner") if isinstance(config.get("runner"), dict) else {"kind": "local"}
runner_kind = str(runner.get("kind") or "local").strip().casefold()
if runner_kind == "http":
url = str(runner.get("url") or "").rstrip("/") + "/configuration/activation/debug"
request_body = {
"base_id": base_id,
"layer": layer,
"mode": "debug",
}
if request_id and fingerprint:
request_body["request_id"] = request_id
request_body["fingerprint"] = fingerprint
body = json.dumps(
request_body,
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"Activation debug runner returned HTTP {exc.code}.",
}
return result if isinstance(result, dict) else {
"status": "runner_error",
"message": f"Activation debug 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": "Activation debug runner returned a non-object response.",
}
infobase = config.get("infobase") if isinstance(config.get("infobase"), dict) else {}
selector_configured = (
sum(bool(str(infobase.get(key) or "").strip()) for key in ("file", "server", "name")) == 1
)
designer_path = str(config.get("designer_path") or "").strip()
try:
designer_available = bool(designer_path and Path(designer_path).is_file())
except OSError:
designer_available = False
ready = bool(selector_configured and designer_available)
debug_acceptance = None
if request_id and fingerprint:
receipt_source = json.dumps(
{
"base_id": base_id,
"layer": layer,
"request_id": request_id,
"fingerprint": fingerprint,
"mode": "debug",
},
ensure_ascii=False,
sort_keys=True,
separators=(",", ":"),
).encode("utf-8")
debug_acceptance = {
"accepted": ready,
"request_id": request_id,
"fingerprint": fingerprint,
"receipt": hashlib.sha256(receipt_source).hexdigest() if ready else None,
}
return {
"schema": "onec_configuration_activation_runner_probe.v1",
"status": "ready" if ready else "not_ready",
"base_id": base_id,
"layer": layer,
"runner": {
"kind": "local",
"reachable": True,
"designer_path_configured": bool(designer_path),
"designer_available": designer_available,
"infobase_selector_configured": selector_configured,
},
"operation": {
"kind": "/UpdateDBCfg" if layer == "base_saved_state" else None,
"execution_supported": False,
"extension_manual_only": layer in {"all", "extension_saved_state"},
},
"debug_acceptance": debug_acceptance,
"execution": {
"mode": "debug",
"performed": False,
"designer_started": False,
"active_configuration_changed": False,
},
}
def _execute_repository(
base_id: str,
config: dict[str, Any],
@@ -483,8 +618,7 @@ def create_lock_request(payload: dict[str, Any]) -> dict[str, Any]:
}
repository_user = requested_repository_user or configured_repository_user
request_id = "rreq-" + uuid.uuid4().hex
with _STATE_LOCK:
state = _read_state()
with _state_transaction() as state:
state.setdefault("requests", {})[request_id] = {
"base_id": base_id,
"layer": layer_id,
@@ -499,7 +633,6 @@ def create_lock_request(payload: dict[str, Any]) -> dict[str, Any]:
"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,
@@ -521,7 +654,21 @@ 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}
result = {"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,
# Surface the manual-confirmation scope at top level. Requiring
# callers to inspect an opaque persisted request made a pending lock
# look context-free and encouraged unsafe confirmation guesses.
"base_id": request.get("base_id"),
"layer_id": request.get("layer_id") or request.get("layer"),
"objects": list(request.get("objects") or []),
"repository_user": request.get("repository_user") or None,
"native_lock_state": "unknown",
"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)
@@ -532,8 +679,7 @@ 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()
with _state_transaction() as 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}
@@ -542,20 +688,284 @@ def cancel_lock_request(payload: dict[str, Any]) -> dict[str, Any]:
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:
"""Legacy JSON path used only for one-time migration to local SQLite."""
return Path(os.environ.get("ONEC_REPOSITORY_STATE_FILE") or "/data/onec-repository-locks.json")
def _read_state() -> dict[str, Any]:
def _state_db_path() -> Path:
"""Adapter-local state database; never points at a configured 1C database."""
configured = os.environ.get("ONEC_ADAPTER_STATE_DB") or os.environ.get("ONEC_ADAPTER_CACHE_DB")
if configured:
return Path(configured)
legacy_override = os.environ.get("ONEC_REPOSITORY_STATE_FILE")
if legacy_override:
return Path(legacy_override).with_suffix(".sqlite")
return Path("/data/adapter-cache.sqlite")
def _empty_state() -> dict[str, Any]:
return {"sessions": {}, "requests": {}, "audit": []}
def _read_legacy_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": []}
return value if isinstance(value, dict) else _empty_state()
except (OSError, json.JSONDecodeError):
state = {"sessions": {}, "requests": {}, "audit": []}
return _empty_state()
def _state_connection() -> sqlite3.Connection:
path = _state_db_path()
path.parent.mkdir(parents=True, exist_ok=True)
conn = sqlite3.connect(path, timeout=30)
conn.row_factory = sqlite3.Row
conn.execute("PRAGMA journal_mode=WAL")
conn.execute("PRAGMA synchronous=NORMAL")
conn.execute("PRAGMA busy_timeout=30000")
conn.execute(
"""
CREATE TABLE IF NOT EXISTS adapter_state_meta (
key TEXT PRIMARY KEY,
value TEXT,
updated_at REAL NOT NULL
)
"""
)
conn.execute(
"""
CREATE TABLE IF NOT EXISTS repository_lock_requests (
request_id TEXT PRIMARY KEY,
base_id TEXT NOT NULL,
layer_id TEXT NOT NULL,
status TEXT NOT NULL,
created_at REAL NOT NULL,
updated_at REAL NOT NULL,
payload_json TEXT NOT NULL
)
"""
)
conn.execute(
"""
CREATE TABLE IF NOT EXISTS repository_lock_sessions (
lock_session_id TEXT PRIMARY KEY,
base_id TEXT NOT NULL,
layer_id TEXT NOT NULL,
status TEXT NOT NULL,
created_at REAL NOT NULL,
updated_at REAL NOT NULL,
payload_json TEXT NOT NULL
)
"""
)
conn.execute(
"""
CREATE TABLE IF NOT EXISTS repository_lock_events (
event_id TEXT PRIMARY KEY,
event TEXT NOT NULL,
occurred_at REAL NOT NULL,
base_id TEXT,
request_id TEXT,
lock_session_id TEXT,
details_json TEXT NOT NULL
)
"""
)
conn.execute(
"CREATE INDEX IF NOT EXISTS idx_repository_lock_requests_base_status "
"ON repository_lock_requests(base_id, status, created_at)"
)
conn.execute(
"CREATE INDEX IF NOT EXISTS idx_repository_lock_sessions_base_status "
"ON repository_lock_sessions(base_id, status, created_at)"
)
conn.execute(
"CREATE INDEX IF NOT EXISTS idx_repository_lock_events_base_time "
"ON repository_lock_events(base_id, occurred_at)"
)
conn.execute(
"""
INSERT INTO adapter_state_meta(key, value, updated_at)
VALUES('adapter_state_schema_version', ?, ?)
ON CONFLICT(key) DO UPDATE SET
value=CASE
WHEN CAST(adapter_state_meta.value AS INTEGER) < CAST(excluded.value AS INTEGER)
THEN excluded.value
ELSE adapter_state_meta.value
END,
updated_at=excluded.updated_at
""",
(str(REPOSITORY_STATE_SCHEMA_VERSION), time.time()),
)
migration = conn.execute(
"SELECT value FROM adapter_state_meta WHERE key='legacy_repository_state_imported'"
).fetchone()
if not migration:
legacy = _read_legacy_state()
_sync_state_to_connection(conn, legacy)
conn.execute(
"INSERT INTO adapter_state_meta(key, value, updated_at) VALUES(?, ?, ?)",
("legacy_repository_state_imported", "1", time.time()),
)
conn.commit()
return conn
def _event_id(row: dict[str, Any], index: int) -> str:
explicit = str(row.get("event_id") or "").strip()
if explicit:
return explicit
source = json.dumps(
{
"index": index,
"event": row.get("event"),
"time": row.get("time"),
"request_id": row.get("request_id"),
"lock_session_id": row.get("lock_session_id"),
"base_id": row.get("base_id"),
},
ensure_ascii=False,
sort_keys=True,
separators=(",", ":"),
)
return "legacy-" + hashlib.sha1(source.encode("utf-8")).hexdigest()
def _load_state_from_connection(conn: sqlite3.Connection) -> dict[str, Any]:
state = _empty_state()
for row in conn.execute("SELECT request_id, payload_json FROM repository_lock_requests"):
try:
payload = json.loads(row["payload_json"])
except (TypeError, json.JSONDecodeError):
continue
if isinstance(payload, dict):
state["requests"][str(row["request_id"])] = payload
for row in conn.execute("SELECT lock_session_id, payload_json FROM repository_lock_sessions"):
try:
payload = json.loads(row["payload_json"])
except (TypeError, json.JSONDecodeError):
continue
if isinstance(payload, dict):
state["sessions"][str(row["lock_session_id"])] = payload
for row in conn.execute(
"SELECT event_id, event, occurred_at, details_json FROM repository_lock_events "
"ORDER BY occurred_at, event_id"
):
try:
payload = json.loads(row["details_json"])
except (TypeError, json.JSONDecodeError):
payload = {}
if not isinstance(payload, dict):
payload = {}
payload.setdefault("event_id", str(row["event_id"]))
payload.setdefault("event", str(row["event"]))
payload.setdefault("time", float(row["occurred_at"]))
state["audit"].append(payload)
return state
def _sync_state_to_connection(conn: sqlite3.Connection, value: dict[str, Any]) -> None:
now = time.time()
for request_id, raw in (value.get("requests") or {}).items():
if not isinstance(raw, dict):
continue
request = dict(raw)
created_at = float(request.get("created_at") or now)
updated_at = float(
request.get("cancelled_at")
or request.get("confirmed_at")
or request.get("closed_at")
or request.get("expired_at")
or created_at
)
conn.execute(
"""
INSERT INTO repository_lock_requests(
request_id, base_id, layer_id, status, created_at, updated_at, payload_json
) VALUES(?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(request_id) DO UPDATE SET
base_id=excluded.base_id,
layer_id=excluded.layer_id,
status=excluded.status,
updated_at=excluded.updated_at,
payload_json=excluded.payload_json
""",
(
str(request_id),
str(request.get("base_id") or ""),
str(request.get("layer_id") or request.get("layer") or "base"),
str(request.get("status") or "unknown"),
created_at,
updated_at,
json.dumps(request, ensure_ascii=False, separators=(",", ":")),
),
)
for session_id, raw in (value.get("sessions") or {}).items():
if not isinstance(raw, dict):
continue
session = dict(raw)
created_at = float(session.get("created_at") or now)
updated_at = float(
session.get("committed_at")
or session.get("released_at")
or session.get("closed_at")
or session.get("expired_at")
or created_at
)
conn.execute(
"""
INSERT INTO repository_lock_sessions(
lock_session_id, base_id, layer_id, status, created_at, updated_at, payload_json
) VALUES(?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(lock_session_id) DO UPDATE SET
base_id=excluded.base_id,
layer_id=excluded.layer_id,
status=excluded.status,
updated_at=excluded.updated_at,
payload_json=excluded.payload_json
""",
(
str(session_id),
str(session.get("base_id") or ""),
str(session.get("layer_id") or session.get("layer") or "base"),
str(session.get("status") or "unknown"),
created_at,
updated_at,
json.dumps(session, ensure_ascii=False, separators=(",", ":")),
),
)
audit = [row for row in (value.get("audit") or []) if isinstance(row, dict)][-5000:]
for index, raw in enumerate(audit):
event = dict(raw)
event_id = _event_id(event, index)
event["event_id"] = event_id
occurred_at = float(event.get("time") or now)
conn.execute(
"""
INSERT OR IGNORE INTO repository_lock_events(
event_id, event, occurred_at, base_id, request_id, lock_session_id, details_json
) VALUES(?, ?, ?, ?, ?, ?, ?)
""",
(
event_id,
str(event.get("event") or "unknown"),
occurred_at,
str(event.get("base_id") or "") or None,
str(event.get("request_id") or "") or None,
str(event.get("lock_session_id") or "") or None,
json.dumps(event, ensure_ascii=False, separators=(",", ":")),
),
)
def _expire_state(state: dict[str, Any]) -> bool:
changed = False
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))
@@ -563,24 +973,57 @@ def _read_state() -> dict[str, Any]:
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
changed = True
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
changed = True
return changed
def _read_state() -> dict[str, Any]:
with _STATE_LOCK:
with _state_connection() as conn:
state = _load_state_from_connection(conn)
if _expire_state(state):
conn.execute("BEGIN IMMEDIATE")
_sync_state_to_connection(conn, state)
conn.commit()
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)
with _STATE_LOCK:
with _state_connection() as conn:
conn.execute("BEGIN IMMEDIATE")
_sync_state_to_connection(conn, value)
conn.commit()
@contextmanager
def _state_transaction() -> Any:
"""Serialize a repository state mutation across adapter processes."""
with _STATE_LOCK:
conn = _state_connection()
try:
conn.execute("BEGIN IMMEDIATE")
state = _load_state_from_connection(conn)
_expire_state(state)
yield state
_sync_state_to_connection(conn, state)
conn.commit()
except Exception:
conn.rollback()
raise
finally:
conn.close()
def _audit(state: dict[str, Any], event: str, **details: Any) -> None:
rows = state.setdefault("audit", [])
rows.append({"event": event, "time": time.time(), **details})
rows.append({"event_id": "revt-" + uuid.uuid4().hex, "event": event, "time": time.time(), **details})
if len(rows) > 5000:
del rows[:-5000]
@@ -659,10 +1102,9 @@ def lock(payload: dict[str, Any]) -> dict[str, Any]:
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)
with _state_transaction() as 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"}
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}
@@ -724,19 +1166,33 @@ def confirm_manual_lock(payload: dict[str, Any]) -> dict[str, Any]:
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)
with _state_transaction() as current_state:
current_request = (current_state.get("requests") or {}).get(request_id) if request_id else None
if request_id and (
not isinstance(current_request, dict)
or current_request.get("base_id") != base_id
or current_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,
}
current_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(current_request, dict):
current_request["status"] = "confirmed_by_user"
current_request["confirmed_at"] = time.time()
current_request["lock_session_id"] = session_id
_audit(current_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)
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"],
@@ -769,8 +1225,7 @@ 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()
with _state_transaction() as 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}
@@ -797,7 +1252,6 @@ def close_manual_lock(payload: dict[str, Any]) -> dict[str, Any]:
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,
@@ -882,6 +1336,18 @@ def support_gate(payload: dict[str, Any]) -> dict[str, Any]:
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):
# See repository_config(): an explicit repository-less base is the
# disposable-test profile. It applies to newly discovered extensions
# as well, so a missing per-extension policy cannot turn a permitted
# test write into a false "unknown support" block.
repository, repository_error = repository_config(base_id, layer_id)
if repository_error is None and isinstance(repository, dict) and repository.get("mode") == "none":
return {
"required": False, "allowed": True,
"status": "not_on_support_inherited_no_repository",
"layer_id": layer_id,
"inherited_from_layer": repository.get("inherited_from_layer"),
}
return {"required": True, "allowed": False, "status": "blocked_support_layer_unknown", "layer_id": layer_id}
support = layer.get("support")
if not isinstance(support, dict):
@@ -952,11 +1418,16 @@ def commit(payload: dict[str, Any]) -> dict[str, Any]:
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}
with _state_transaction() as current_state:
current_session = (current_state.get("sessions") or {}).get(session_id)
if not isinstance(current_session, dict) or current_session.get("status") != "acquired":
return {"schema": "onec_repository_commit.v1", "method": METHOD_COMMIT, "status": "blocked", "error": "lock_session_not_acquired", "lock_session_id": session_id}
current_session["status"] = "acquired" if payload.get("keep_locked") is True else "committed"
current_session["committed_at"] = time.time()
current_session["commit_comment"] = str(plan["comment"])
final_status = str(current_session["status"])
committed_objects = current_session.get("objects")
return {"schema": "onec_repository_commit.v1", "method": METHOD_COMMIT, "status": final_status, "lock_session_id": session_id, "committed": committed_objects, "keep_locked": payload.get("keep_locked") is True, "execution": executed}
def unlock(payload: dict[str, Any]) -> dict[str, Any]:
@@ -977,10 +1448,14 @@ def unlock(payload: dict[str, Any]) -> dict[str, Any]:
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}
with _state_transaction() as current_state:
current_session = (current_state.get("sessions") or {}).get(session_id)
if not isinstance(current_session, dict) or current_session.get("status") != "acquired":
return {"schema": "onec_repository_unlock.v1", "method": METHOD_UNLOCK, "status": "blocked", "error": "lock_session_not_acquired", "lock_session_id": session_id}
current_session["status"] = "released"
current_session["released_at"] = time.time()
released_objects = current_session.get("objects")
return {"schema": "onec_repository_unlock.v1", "method": METHOD_UNLOCK, "status": "released", "lock_session_id": session_id, "released": released_objects, "execution": executed}
def call(method: str, payload: dict[str, Any]) -> dict[str, Any]: