Complete name-first 1C adapter saved-state support

This commit is contained in:
2026-07-26 16:39:53 +03:00
parent b8c62fa8fa
commit aed134d817
44 changed files with 15436 additions and 697 deletions
+10
View File
@@ -28,8 +28,18 @@ ONEC_INFOBASE_USER_ADMIN_TOKEN_UPO_TEST=
ONEC_INFOBASE_USER_ADMIN_ALLOW_UNAUTHENTICATED=false
ONEC_ADAPTER_CACHE_DB=/data/adapter-cache.sqlite
# Adapter-owned runtime state (jobs, progress, restart diagnostics). This is
# local SQLite and must not point to a 1C SQL database. Defaults to CACHE_DB.
ONEC_ADAPTER_STATE_DB=/data/adapter-cache.sqlite
# Legacy JSON job store is read once for migration only.
ONEC_ADAPTER_JOB_STORE=/data/adapter-jobs.json
ONEC_ADAPTER_BACKUP_DIR=/data/adapter-apply-backups
ONEC_ADAPTER_WRITE_LEARNING_DIR=/data/adapter-write-learning
ONEC_ADAPTER_JOB_TIMEOUT_SECONDS=240
ONEC_ADAPTER_FULL_TIMEOUT_SECONDS=600
ONEC_ADAPTER_SECTION_TIMEOUT_SECONDS=180
# Run non-stateful background calls in killable child processes.
ONEC_ADAPTER_JOB_PROCESS_ISOLATION=true
# Optional POSIX child-process limits; 0 keeps the platform/container limit.
ONEC_ADAPTER_JOB_MEMORY_LIMIT_MB=0
ONEC_ADAPTER_JOB_CPU_LIMIT_SECONDS=0
+39 -1
View File
@@ -223,6 +223,7 @@ Current live methods:
- `metadata.objects.list`
- `metadata.object.get`
- `metadata.object.properties`
- `metadata.object.property.write`
- `metadata.object.decode`
- `metadata.object.parts`
- `metadata.object.modules`
@@ -260,6 +261,13 @@ qualified metadata names such as `Обработка.<Name>` or `Document.<Name>
Client, MCP, and agent code must not add conditions for concrete object names;
the adapter owns generic selector normalization.
Saved-state client calls use the same name-first selectors together with
`layer=base_saved_state|extension_saved_state`. SQL tables, file names, GUID
owners, and module handles are diagnostic continuations exposed only with
`include_storage=true`. Every public RPC follow-up is shaped as
`{"method": "...", "params": {...}}`; `payload` is not the arguments field of
`next_call` or `next_resolution`.
Layer write policy:
- `Config` and `ConfigCAS` are **active-applied** and must be treated as read-only in adapter workflows.
@@ -293,6 +301,26 @@ metadata kind. It selects a kind-specific SQL decoder for `Configuration`,
semantic profile. XML exports are analysis evidence only and are never a
runtime source for this method or any other adapter method.
`metadata.object.property.write` is the name-first saved-state writer for the
standard identity properties `synonym` and `comment`. Pass a public object
`ref` or `kind` + `name`. For an existing attribute, tabular section,
dimension, or resource, also pass `member_ref` or `member_kind` +
`member_name`; the adapter resolves the exact parent/member GUIDs and
serialized tree path internally. The method supports `plan`, `apply`,
`apply_and_verify`, and `apply_and_rollback`, requires explicit saved-state
gates, and never writes active `Config`/`ConfigCAS`. Renaming an object and
member, adding/removing collection items, and adding a new synonym locale
remain intentionally disabled.
`metadata.object.member.add` adds one new object requisite or tabular-section
column (`Attribute`) by cloning an existing attribute in the same collection.
The caller passes only
`template_member_ref`, `new_member_name`, and optionally synonym/comment; the
adapter generates the GUID, preserves the template's type/settings, appends to
the exact declared collection, and verifies the new identity after apply.
Arbitrary type construction and deletion are not supported by this first
structural route.
Managed form bodies in base `Config` are resolved from the public form GUID to
the sibling `<guid>.0` SQL payload. Command-bar buttons expose public command
names when their SQL binding points to a common command or a recognized
@@ -468,7 +496,17 @@ capabilities).
`metadata.adapter.audit` reports recognized metadata kinds, public kind counts,
missing supported kinds when `include_missing=true`, and unmapped DBNames roles
when `include_unmapped=true`.
when `include_unmapped=true`. Missing entries are marked
`presence_status=supported_absent_in_selected_base`; this is a statement about
the selected infobase, not a claim that the adapter lacks that kind.
Use `python scripts/check_1c_metadata_kind_fixtures.py --live` from the
repository root to check the reproducible rare-kind fixtures. The checker does
not write SQL or create metadata. The fixture manifest pins the exact Designer
version and external reference commit. The companion
`scripts/export_1c_extension_sources.ps1` performs a read-only `test2` source
export with operating-system integrated authentication and exposes no
infobase-user or credential arguments.
`codec.decode` and `codec.encode` are low-level lossless helpers. A no-op encode
from a live source keeps the original bytes exactly; modified text/tree payloads
File diff suppressed because it is too large Load Diff
+5 -7
View File
@@ -74,12 +74,7 @@ function openEditor(base = null) {
$("baseId").value = base?.base_id || ""; $("server").value = base?.server || "";
$("database").value = base?.database || ""; $("user").value = base?.user || "";
$("password").value = ""; $("passwordEnv").value = base?.password_env || "";
$("repositoryEnabled").checked = Boolean(base?.repository);
$("repositoryBackend").value = base?.repository?.backend || "direct";
$("repositoryLayer").value = base?.repository?.layer || "base";
$("repositoryLockMode").value = base?.repository?.lock_mode || "automatic";
$("repositoryBridgeId").value = base?.repository?.bridge_id || "";
$("repositoryUser").value = base?.repository?.repository_user || "";
$("developmentLayers").value = JSON.stringify(base?.development_layers || { base: { repository: { mode: "none", connection_state: "not_configured" }, support: { mode: "unknown" } } }, null, 2);
$("passwordHint").textContent = base?.has_password ? "Пароль уже задан. Оставьте пустым, чтобы сохранить текущий." : "Задайте пароль или переменную окружения.";
$("editor").showModal(); setTimeout(() => $("baseId").focus(), 30);
}
@@ -88,7 +83,10 @@ async function save(event) {
event.preventDefault();
if (!$("baseForm").reportValidity()) return;
const original = $("originalId").value;
const payload = { base_id: $("baseId").value.trim(), server: $("server").value.trim(), database: $("database").value.trim(), user: $("user").value.trim(), password: $("password").value, password_env: $("passwordEnv").value.trim(), repository: $("repositoryEnabled").checked ? { backend: $("repositoryBackend").value, layer: $("repositoryLayer").value, lock_mode: $("repositoryLockMode").value, bridge_id: $("repositoryBridgeId").value.trim(), repository_user: $("repositoryUser").value.trim() } : { enabled: false } };
let developmentLayers;
try { developmentLayers = JSON.parse($("developmentLayers").value || "{}"); }
catch (_) { notice("Слои разработки должны быть корректным JSON.", true); return; }
const payload = { base_id: $("baseId").value.trim(), server: $("server").value.trim(), database: $("database").value.trim(), user: $("user").value.trim(), password: $("password").value, password_env: $("passwordEnv").value.trim(), development_layers: developmentLayers, repository: { enabled: false } };
$("saveButton").disabled = true;
try {
await request(original ? `/admin/api/bases/${encodeURIComponent(original)}` : "/admin/api/bases", { method: original ? "PUT" : "POST", body: JSON.stringify(payload) });
+1 -6
View File
@@ -43,12 +43,7 @@
<label>SQL-логин<input id="user" required autocomplete="username" placeholder="onec_reader"></label>
<label class="wide">Пароль<input id="password" type="password" autocomplete="new-password" placeholder="Оставьте пустым, чтобы не менять"><small id="passwordHint">Пароль сохраняется в защищённом runtime-файле и никогда не отображается.</small></label>
<label class="wide">Или переменная окружения<input id="passwordEnv" placeholder="ONEC_SQL_PASSWORD_UPO_TEST"><small>Если заполнено, имеет приоритет над введённым паролем.</small></label>
<label class="wide"><input id="repositoryEnabled" type="checkbox"> Конфигурация подключена к хранилищу</label>
<label>Доступ к хранилищу<select id="repositoryBackend"><option value="direct">Прямой</option><option value="karman_bridge">Через Карман</option></select></label>
<label>Слой<select id="repositoryLayer"><option value="base">Основная конфигурация</option><option value="extension">Расширение</option></select></label>
<label>Захват объектов<select id="repositoryLockMode"><option value="manual">Вручную пользователем (SQL-only)</option><option value="automatic" disabled>Через внешнюю 1С (следующая версия)</option></select></label>
<label>ID моста (из настройки)<input id="repositoryBridgeId" placeholder="Необязательно"></label>
<label>Пользователь хранилища<input id="repositoryUser" autocomplete="off" placeholder="Например, adm"></label>
<label class="wide">Слои разработки (JSON)<textarea id="developmentLayers" rows="12" spellcheck="false" placeholder='{"base":{"repository":{"mode":"manual"},"support":{"mode":"unknown"}}}'></textarea><small>Основная конфигурация задаётся ключом base, каждое расширение — отдельным ключом extension:&lt;GUID&gt;. repository.mode: none/manual/automatic/unknown; support.mode: none/editable/locked/rules/unknown.</small></label>
</div>
<div class="dialog-actions"><button value="cancel" class="secondary">Отмена</button><button id="saveButton" value="default" class="primary">Сохранить</button></div>
</form>
+266 -6
View File
@@ -16,6 +16,164 @@ paths:
responses:
"200":
description: Connector health.
/methods:
get:
operationId: listAdapterMethods
summary: Runtime adapter method registry
description: Returns every supported RPC method, transport, description, selector capabilities, and input schema known by the running adapter.
parameters:
- name: method
in: query
required: false
schema:
type: string
description: Optional exact method name.
responses:
"200":
description: Runtime registry generated from the adapter method definitions.
content:
application/json:
schema:
$ref: "#/components/schemas/AdapterMethodsResponse"
/rpc:
post:
operationId: callAdapterMethod
summary: Universal adapter RPC
description: Calls any method advertised by GET /methods. The selected method's input_schema from /methods defines payload.
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/AdapterRpcRequest"
responses:
"200":
description: Method-specific adapter response. Application errors are returned as structured JSON statuses.
content:
application/json:
schema:
type: object
additionalProperties: true
/extensions:
get:
operationId: listExtensions
parameters:
- $ref: "#/components/parameters/BaseId"
responses:
"200":
$ref: "#/components/responses/AdapterMethodResponse"
/metadata/object/templates:
post:
operationId: listObjectTemplates
requestBody:
$ref: "#/components/requestBodies/AdapterMethodPayload"
responses:
"200":
$ref: "#/components/responses/AdapterMethodResponse"
/metadata/object/template-details:
post:
operationId: getObjectTemplateDetails
requestBody:
$ref: "#/components/requestBodies/AdapterMethodPayload"
responses:
"200":
$ref: "#/components/responses/AdapterMethodResponse"
/metadata/object/form-details:
post:
operationId: getObjectFormDetails
requestBody:
$ref: "#/components/requestBodies/AdapterMethodPayload"
responses:
"200":
$ref: "#/components/responses/AdapterMethodResponse"
/metadata/object/commands:
post:
operationId: listObjectCommands
requestBody:
$ref: "#/components/requestBodies/AdapterMethodPayload"
responses:
"200":
$ref: "#/components/responses/AdapterMethodResponse"
/metadata/object/special-details:
post:
operationId: getObjectSpecialDetails
requestBody:
$ref: "#/components/requestBodies/AdapterMethodPayload"
responses:
"200":
$ref: "#/components/responses/AdapterMethodResponse"
/metadata/module-owner-cache/prune:
post:
operationId: pruneModuleOwnerCache
requestBody:
$ref: "#/components/requestBodies/AdapterMethodPayload"
responses:
"200":
$ref: "#/components/responses/AdapterMethodResponse"
/code/search:
post:
operationId: searchCode
requestBody:
$ref: "#/components/requestBodies/AdapterMethodPayload"
responses:
"200":
$ref: "#/components/responses/AdapterMethodResponse"
/code/read:
post:
operationId: readCode
requestBody:
$ref: "#/components/requestBodies/AdapterMethodPayload"
responses:
"200":
$ref: "#/components/responses/AdapterMethodResponse"
/templates/bindings:
post:
operationId: getTemplateBindings
requestBody:
$ref: "#/components/requestBodies/AdapterMethodPayload"
responses:
"200":
$ref: "#/components/responses/AdapterMethodResponse"
/diagnostics/call-chain:
post:
operationId: diagnoseCallChain
requestBody:
$ref: "#/components/requestBodies/AdapterMethodPayload"
responses:
"200":
$ref: "#/components/responses/AdapterMethodResponse"
/access/users/search:
post:
operationId: searchAccessUsers
requestBody:
$ref: "#/components/requestBodies/AdapterMethodPayload"
responses:
"200":
$ref: "#/components/responses/AdapterMethodResponse"
/access/object/roles:
post:
operationId: getAccessObjectRoles
requestBody:
$ref: "#/components/requestBodies/AdapterMethodPayload"
responses:
"200":
$ref: "#/components/responses/AdapterMethodResponse"
/access/object/subjects:
post:
operationId: getAccessObjectSubjects
requestBody:
$ref: "#/components/requestBodies/AdapterMethodPayload"
responses:
"200":
$ref: "#/components/responses/AdapterMethodResponse"
/access/rls/discover:
post:
operationId: discoverAccessRls
requestBody:
$ref: "#/components/requestBodies/AdapterMethodPayload"
responses:
"200":
$ref: "#/components/responses/AdapterMethodResponse"
/metadata/kinds:
get:
operationId: getMetadataKinds
@@ -1734,7 +1892,11 @@ paths:
type: string
enum: [group, user_set, object, set, all]
default: all
description: BSP access key area to page through.
description: BSP access key area to page through; this is not a 1C metadata kind.
area:
type: string
enum: [group, user_set, object, set, all]
description: Alias of kind.
group:
type: string
description: Filter group access keys by normalized group id.
@@ -1744,9 +1906,21 @@ paths:
user_set:
type: string
description: Filter user-set access keys by normalized user set id.
object_ref:
type: string
description: Public 1C metadata object ref, for example Справочники.Номенклатура.
record_ref:
type: string
description: Concrete 1C application-data record reference.
object:
type: string
description: Filter object access keys by normalized object id.
description: Legacy raw BSP object filter; prefer object_ref plus record_ref.
object_id:
type: string
description: Legacy raw BSP record id; prefer record_ref.
object_sql_number:
type: integer
description: Legacy physical DBNames number; prefer object_ref.
access_set:
type: string
description: Filter access-set keys by normalized access set id.
@@ -1795,9 +1969,21 @@ paths:
properties:
base_id:
type: string
object_ref:
type: string
description: Public 1C metadata object ref, for example Справочники.Номенклатура.
record_ref:
type: string
description: Concrete 1C application-data record reference.
object:
type: string
description: Optional normalized object ref filter.
description: Legacy raw BSP object filter; prefer object_ref plus record_ref.
object_id:
type: string
description: Legacy raw BSP record id; prefer record_ref.
object_sql_number:
type: integer
description: Legacy physical DBNames number; prefer object_ref.
access_key:
type: string
description: Optional access key filter.
@@ -1836,15 +2022,21 @@ paths:
properties:
base_id:
type: string
object_ref:
type: string
description: Public 1C metadata object ref, for example Справочники.Номенклатура.
record_ref:
type: string
description: Concrete 1C application-data record reference.
object:
type: string
description: Normalized object ref from access_object_keys.object.
description: Legacy raw BSP object filter; prefer object_ref plus record_ref.
object_id:
type: string
description: Data record id from access_object_keys.object_id.
description: Legacy raw BSP record id; prefer record_ref.
object_sql_number:
type: integer
description: Optional SQL metadata number to narrow object key rows.
description: Legacy physical DBNames number; prefer object_ref.
access_key:
type: string
description: Access key id to explain directly.
@@ -2020,6 +2212,27 @@ paths:
"200":
description: Risk findings for role -> profile -> access group -> user audit chains.
components:
requestBodies:
AdapterMethodPayload:
required: true
content:
application/json:
schema:
type: object
additionalProperties: true
examples:
selectorByName:
value:
base_id: <base_id>
ref: Документы.<ИмяОбъекта>
responses:
AdapterMethodResponse:
description: Method-specific adapter response with a structured status.
content:
application/json:
schema:
type: object
additionalProperties: true
parameters:
BaseId:
name: base_id
@@ -2033,6 +2246,53 @@ components:
type: http
scheme: bearer
schemas:
AdapterRpcRequest:
type: object
required: [method]
additionalProperties: false
properties:
method:
type: string
minLength: 1
description: Exact method name advertised by GET /methods.
payload:
type: object
default: {}
additionalProperties: true
description: Method-specific input. Use the matching input_schema returned by GET /methods.
AdapterMethod:
type: object
required: [name, transport, description]
additionalProperties: true
properties:
name:
type: string
transport:
type: string
description:
type: string
selector_capabilities:
type: object
additionalProperties: true
input_schema:
type: object
additionalProperties: true
AdapterMethodsResponse:
type: object
required: [schema, contract_version, methods, count]
properties:
schema:
type: string
const: onec_adapter_methods.v1
contract_version:
type: string
methods:
type: array
items:
$ref: "#/components/schemas/AdapterMethod"
count:
type: integer
minimum: 0
MetadataKind:
type: string
enum:
+308 -42
View File
@@ -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")):