Complete name-first 1C adapter saved-state support
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
@@ -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) });
|
||||
|
||||
@@ -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:<GUID>. 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>
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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