WeKnora deployment source with the reviewed wiki identity, summary and recovery fixes
Anydoc / anydoc parser engine (push) Canceled after 0s
App / Format, vet, test, and build (push) Canceled after 0s
CLI / test (macos-latest / go 1.26) (push) Canceled after 0s
CLI / test (ubuntu-latest / go 1.26) (push) Canceled after 0s
CLI / test (windows-latest / go 1.26) (push) Canceled after 0s
Build and Push Docker Image / build-ui (push) Canceled after 0s
Build and Push Docker Image / build-docreader (push) Canceled after 0s
Build and Push Docker Image / build-sandbox (push) Canceled after 0s
Build and Push Docker Image / build-app (amd64, linux/amd64, ubuntu-latest) (push) Canceled after 0s
Build and Push Docker Image / build-app (arm64, linux/arm64, ubuntu-24.04-arm) (push) Canceled after 0s
DocReader / Python tests and Go client tests (push) Canceled after 0s
Build & Publish (dsh-weknora) / Typecheck, test, pack (push) Canceled after 0s
Build & Publish (dsh-weknora) / End-to-end inside dsh (pinned) (push) Canceled after 0s
Build & Publish (dsh-weknora) / End-to-end inside dsh (latest, advisory) (push) Canceled after 0s
Frontend / Test, type-check, and build (push) Canceled after 0s
Go Lint Cache / warm lint cache (.) (push) Canceled after 0s
Go Lint Cache / warm lint cache (cli) (push) Canceled after 0s
Go Lint Cache / warm lint cache (client) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.10) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.11) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.12) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.13) (push) Canceled after 0s
Build and Push Docker Image / merge (push) Canceled after 0s
Build & Publish (dsh-weknora) / Publish to npm (push) Canceled after 0s
Build & Publish (mcp-server) / Build sdist + wheel (push) Canceled after 0s
Build & Publish (mcp-server) / Publish to PyPI (push) Canceled after 0s

This commit is contained in:
yawan
2026-09-22 16:19:06 +08:00
commit f7fc992f57
3929 changed files with 1006607 additions and 0 deletions
+474
View File
@@ -0,0 +1,474 @@
"""Read legacy Agent/Markdown assets and emit guarded acceptance SQL.
The caller owns the read-only snapshot and target transaction. Source prompt
text is opaque data: it is preserved verbatim, never evaluated or logged here.
Plans and SQL contain private source bodies; persist only safe_asset_summary.
"""
from __future__ import annotations
import hashlib
import json
import re
import sqlite3
import uuid
from weknora_identity_etl import ADMIN_TARGET_ID, IDENTITY_LEDGER
from weknora_model_etl import resolve_agent_model
SOURCE = "yawan-workbench"
TARGET_DATABASE = "yawan_native_acceptance_20260915"
TARGET_TENANT = 10059
SOURCE_ORGANIZATION = 1
LEDGER = "yawan_legacy_asset_mappings"
BINDING_LEDGER = "yawan_legacy_asset_bindings"
NAMESPACE = uuid.UUID("8b8041ef-9cd6-4803-96ca-17fe215ae948")
AGENT_COLUMNS = ("id", "name", "role", "desc", "content_tags", "kb_ids", "fusion_agent_ids",
"model_provider_id", "model_name", "model_catalog_id", "prompt_template_id",
"allowed_tools", "rules", "rules_version", "status", "builtin", "organization_id",
"owner_user_id", "visibility", "created_at", "runtime_snapshot", "persona_json",
"digital_human_id", "kb_category_ids")
SKILL_COLUMNS = ("id", "name", "desc", "instructions", "enabled", "builtin", "version",
"organization_id", "owner_user_id", "visibility", "created_at")
TEMPLATE_COLUMNS = ("id", "name", "desc", "role_prompt", "task_template", "constraints", "examples",
"output_schema", "style", "version", "enabled", "builtin", "organization_id",
"owner_user_id", "visibility", "created_at", "updated_at")
AGENT_JSON = {"content_tags": list, "kb_ids": list, "fusion_agent_ids": list, "allowed_tools": list,
"runtime_snapshot": dict, "persona_json": dict, "kb_category_ids": dict}
SOURCE_POLICY = "private_bodies_remain_private; no_legacy_grants_inferred"
def asset_id(kind: str, source_id: int) -> str:
if kind not in ("agent", "skill", "template") or type(source_id) is not int or source_id <= 0:
raise ValueError("Invalid source asset identity")
return str(uuid.uuid5(NAMESPACE, f"{SOURCE}:tenant:{TARGET_TENANT}:organization:1:{kind}:{source_id}"))
def _read(db: sqlite3.Connection, table: str, columns: tuple[str, ...]) -> list[dict]:
return [dict(zip(columns, row)) for row in db.execute(f"SELECT {','.join(columns)} FROM {table} ORDER BY id")]
def _json(value, expected: type):
parsed = expected() if value is None else json.loads(value)
if not isinstance(parsed, expected):
raise ValueError("Legacy asset JSON has an incompatible type")
return parsed
def _uuid(value) -> str:
if not isinstance(value, str):
raise ValueError("Target mapping requires a canonical UUID")
try:
if str(uuid.UUID(value)) != value:
raise ValueError
except ValueError:
raise ValueError("Target mapping requires a canonical UUID") from None
return value
def _mapping(mapping: dict) -> dict[str, str]:
if not isinstance(mapping, dict):
raise ValueError("Expected a source ID to native ID mapping")
result = {}
for key, value in mapping.items():
normalized = str(key)
if not normalized.isdigit() or int(normalized) <= 0 or normalized != str(int(normalized)):
raise ValueError("Mapping contains an invalid source ID")
if normalized in result:
raise ValueError("Duplicate source mapping identity")
result[normalized] = _uuid(value)
if len(set(result.values())) != len(result):
raise ValueError("Target mapping is not one-to-one")
return result
def _owner(source: dict, identity_mapping: dict) -> tuple[int, str]:
if (source.get("organization_id") != SOURCE_ORGANIZATION
or type(source.get("owner_user_id")) is not int or source["owner_user_id"] < 0):
raise ValueError("Legacy asset owner or organization is invalid")
source_owner = source["owner_user_id"] or 1
if str(source_owner) not in identity_mapping:
raise ValueError("Legacy asset owner has no durable identity mapping")
return source_owner, identity_mapping[str(source_owner)]
def _source_flags(source: dict) -> None:
if (source.get("visibility") not in ("private", "global")
or source.get("builtin") not in (0, 1)):
raise ValueError("Legacy asset visibility or flags need separate review")
if (not isinstance(source.get("name"), str) or not source["name"].strip()
or len(source["name"]) > 255 or "\x00" in source["name"]):
raise ValueError("Legacy asset name is invalid")
def _target_agent(source: dict, identities: dict, kbs: dict, models: dict, binding_ids: list[str]) -> tuple[dict, dict]:
_source_flags(source)
_, owner = _owner(source, identities)
if source["status"] not in ("active", "disabled"):
raise ValueError("Legacy Agent status needs separate review")
if not isinstance(source["rules"], str) or "\x00" in source["rules"]:
raise ValueError("Legacy Agent rules are invalid")
for field, expected in AGENT_JSON.items():
if not isinstance(source[field], expected):
raise ValueError("Legacy Agent configuration type differs")
if source["role"] not in ("", "general", "coordinator", "production", "qc", "service", "training_map"):
raise ValueError("Legacy Agent role is invalid")
tags = source["content_tags"]
if (len(tags) > 32 or any(not isinstance(tag, str) or not tag.strip() or len(tag) > 64 or "\x00" in tag for tag in tags)
or len(set(tags)) != len(tags)):
raise ValueError("Legacy Agent content tags are invalid")
fusions = source["fusion_agent_ids"]
if (len(fusions) > 8 or any(type(value) is not int or value <= 0 or value == source["id"] for value in fusions)
or len(set(fusions)) != len(fusions) or (fusions and source["role"] != "production")):
raise ValueError("Legacy Agent fusion configuration is invalid")
if source["kb_category_ids"]:
raise ValueError("Legacy Agent category restrictions require native mapping before execution")
if len(binding_ids) > 32:
raise ValueError("Legacy Agent exceeds native instruction skill binding limit")
kb_ids = source["kb_ids"]
if (any(type(k) is not int or k <= 0 or str(k) not in kbs for k in kb_ids)
or len(set(kb_ids)) != len(kb_ids)):
raise ValueError("Legacy Agent has an unmapped or duplicate knowledge base")
routing = resolve_agent_model(models, source)
# Native QA gives textual assets an execution path without granting legacy
# runtime tools (handoff/memory/upstream reads have no equivalent contract).
config = {"agent_mode": "quick-answer", "agent_type": "custom", "system_prompt": source["rules"] or "",
"role": source["role"], "content_tags": tags,
"fusion_agent_ids": [asset_id("agent", value) for value in fusions],
"model_id": routing["target_model_id"], "kb_selection_mode": "selected" if kb_ids else "none",
"knowledge_bases": [kbs[str(k)] for k in kb_ids], "instruction_skill_ids": binding_ids,
"skills_selection_mode": "none", "selected_skills": [], "allowed_tools": [],
"mcp_selection_mode": "none", "mcp_services": [], "sandbox_config_id": "",
"web_search_enabled": False, "citation_enabled": True}
return {"id": asset_id("agent", source["id"]), "tenant_id": TARGET_TENANT, "name": source["name"],
"description": source["desc"] or "", "created_by": owner, "is_builtin": False,
"visibility": source["visibility"], "enabled": source["status"] == "active", "config": config}, routing
def _target_skill(source: dict, identities: dict) -> dict:
_source_flags(source)
_, owner = _owner(source, identities)
if source["enabled"] not in (0, 1) or type(source["version"]) is not int or source["version"] < 1:
raise ValueError("Legacy skill enabled/version flags are invalid")
instructions = source["instructions"]
if not isinstance(instructions, str) or not instructions.strip() or "\x00" in instructions:
raise ValueError("Legacy skill instructions are invalid")
if len(instructions.encode("utf-8")) > 256 * 1024:
raise ValueError("Legacy skill exceeds the native import size limit")
return {"id": asset_id("skill", source["id"]), "tenant_id": TARGET_TENANT, "name": source["name"],
"description": source["desc"] or "", "created_by": owner, "version": str(source["version"]),
"instructions": instructions, "instruction_only": True, "visibility": source["visibility"],
"enabled": bool(source["enabled"]), "bundle_ref": "",
"bundle_sha256": hashlib.sha256(instructions.encode()).hexdigest()}
def _binding_review(agent: dict, skill: dict) -> list[str]:
review = []
if not agent["enabled"]:
review.append("agent_disabled_preserved")
if not skill["enabled"]:
review.append("skill_disabled_preserved")
if skill["visibility"] == "private" and skill["created_by"] != agent["created_by"]:
review.append("blocked_private_owner_mismatch")
if skill["visibility"] == "private" and agent["visibility"] == "global":
review.append("global_agent_private_skill_requires_user_access_check")
return review
def _binding_effective(agent_owner: str, skill: dict) -> bool:
return skill["visibility"] == "global" or skill["created_by"] == agent_owner
def asset_plan(db: sqlite3.Connection, identity_mapping: dict, kb_mapping: dict, model_mapping: dict) -> dict:
"""Build a body-bearing plan; mapping values must come from target ledgers.
identity_mapping / kb_mapping are {legacy_id: native_uuid}. model_mapping
is the safe model_plan result. No target writes or credential reads occur.
"""
identities, kbs = _mapping(identity_mapping), _mapping(kb_mapping)
if identities.get("1") != ADMIN_TARGET_ID:
raise ValueError("Legacy owner zero must map through the pinned administrator")
# Shared grant semantics require a separate native mapping. Preserve no
# inferred public access if the source changes while preparing this batch.
grants = {t: _read(db, t, ("id", "agent_id" if t == "agentgrant" else "skill_id",
"user_id", "permission", "enabled", "created_at"))
for t in ("agentgrant", "agentskillgrant")}
if any(grants.values()):
raise ValueError("Source explicit grants require a reviewed native mapping")
sources = _read(db, "agent", AGENT_COLUMNS)
skills = []
for source in _read(db, "agentskill", SKILL_COLUMNS):
skills.append({"source": source, "target": _target_skill(source, identities)})
skill_by_id = {x["source"]["id"]: x for x in skills}
if len(skill_by_id) != len(skills):
raise ValueError("Duplicate source skill identity")
bindings = _read(db, "agentskillbinding", ("id", "agent_id", "skill_id"))
by_agent, pairs, binding_source_ids = {}, set(), set()
source_agents = {a["id"] for a in sources}
source_owners = {a["id"]: _owner(a, identities)[1] for a in sources}
for binding in bindings:
pair = (binding["agent_id"], binding["skill_id"])
if (binding["agent_id"] not in source_agents or binding["skill_id"] not in skill_by_id
or pair in pairs or binding["id"] in binding_source_ids):
raise ValueError("Legacy binding is duplicated or refers to a missing asset")
pairs.add(pair)
binding_source_ids.add(binding["id"])
if _binding_effective(source_owners[binding["agent_id"]], skill_by_id[binding["skill_id"]]["target"]):
by_agent.setdefault(binding["agent_id"], []).append(asset_id("skill", binding["skill_id"]))
agents = []
for source in sources:
for field, expected in AGENT_JSON.items():
source[field] = _json(source[field], expected)
target, routing = _target_agent(source, identities, kbs, model_mapping, by_agent.get(source["id"], []))
agents.append({"source": source, "target": target, "model_resolution": routing,
"compatibility": ["source_rules_verbatim_in_system_prompt", "native_quick_answer_execution",
"legacy_runtime_orchestration_not_reimplemented", "legacy_builtin_becomes_managed_custom_asset"]})
agent_by_id = {x["source"]["id"]: x for x in agents}
templates = _read(db, "agentprompttemplate", TEMPLATE_COLUMNS)
for template in templates:
_source_flags(template)
_owner(template, identities)
template_ids = {t["id"] for t in templates}
for agent in agents:
source = agent["source"]
if source["prompt_template_id"] is not None and source["prompt_template_id"] not in template_ids:
raise ValueError("Legacy Agent references a missing prompt template")
if any(a not in source_agents for a in source["fusion_agent_ids"]):
raise ValueError("Legacy Agent references a missing fusion Agent")
if any(agent_by_id[a]["source"]["role"] != "production" for a in source["fusion_agent_ids"]):
raise ValueError("Legacy Agent fusion target is not a production Agent")
mapped_bindings = []
for source in bindings:
agent, skill = agent_by_id[source["agent_id"]]["target"], skill_by_id[source["skill_id"]]["target"]
mapped_bindings.append({"source": source, "agent_id": agent["id"], "skill_id": skill["id"],
"effective": _binding_effective(agent["created_by"], skill),
"review": _binding_review(agent, skill)})
# Later identity imports may include deleted users who own none of these assets.
owners = {"1"} | {str(source["owner_user_id"] or 1) for source in sources + templates}
owners.update(str(item["source"]["owner_user_id"] or 1) for item in skills)
identities = {key: value for key, value in identities.items() if key in owners}
result = {"schema_version": 1, "source": SOURCE, "target_database": TARGET_DATABASE,
"target_tenant_id": TARGET_TENANT, "source_organization_id": SOURCE_ORGANIZATION,
"identity_mapping": identities, "kb_mapping": kbs, "model_mapping": model_mapping,
"agents": agents, "skills": skills, "bindings": mapped_bindings,
"prompt_templates": templates, "grants": grants, "access_policy": SOURCE_POLICY,
"repeat_policy": "exact_ledger_replay_preserves_native_edits_and_deletion",
"unsupported_fields": ["prompt_template_id", "allowed_tools",
"rules_version", "builtin", "created_at", "runtime_snapshot", "persona_json",
"digital_human_id", "kb_category_ids"],
"ledgers": {"assets": LEDGER, "bindings": BINDING_LEDGER}}
_validate_plan(result)
return result
def _validate_plan(plan: dict) -> None:
if (plan.get("schema_version") != 1 or plan.get("source") != SOURCE
or plan.get("target_database") != TARGET_DATABASE or plan.get("target_tenant_id") != TARGET_TENANT
or plan.get("source_organization_id") != SOURCE_ORGANIZATION or plan.get("access_policy") != SOURCE_POLICY):
raise ValueError("Asset migration scope or access policy differs")
identities, kbs = _mapping(plan["identity_mapping"]), _mapping(plan["kb_mapping"])
if identities.get("1") != ADMIN_TARGET_ID or any(plan["grants"].values()):
raise ValueError("Asset migration identity or explicit grant policy differs")
template_ids = set()
for template in plan["prompt_templates"]:
_source_flags(template)
_owner(template, identities)
if template["id"] in template_ids:
raise ValueError("Duplicate legacy prompt template")
template_ids.add(template["id"])
agents, skills, seen = {}, {}, set()
for item in plan["skills"]:
source, target = item["source"], item["target"]
if target != _target_skill(source, identities) or target["id"] in seen:
raise ValueError("Asset plan changes skill identity, body or access")
seen.add(target["id"])
skills[source["id"]] = target
source_owners = {item["source"]["id"]: _owner(item["source"], identities)[1] for item in plan["agents"]}
by_agent, pairs, binding_ids = {}, set(), set()
for binding in plan["bindings"]:
source = binding["source"]
pair = (source["agent_id"], source["skill_id"])
if (pair in pairs or source["id"] in binding_ids or source["agent_id"] not in source_owners or source["skill_id"] not in skills
or binding["skill_id"] != asset_id("skill", source["skill_id"])
or binding["agent_id"] != asset_id("agent", source["agent_id"])):
raise ValueError("Asset plan changes binding identity")
pairs.add(pair)
binding_ids.add(source["id"])
effective = _binding_effective(source_owners[source["agent_id"]], skills[source["skill_id"]])
if binding["effective"] is not effective:
raise ValueError("Asset plan changes binding access disposition")
if effective:
by_agent.setdefault(source["agent_id"], []).append(binding["skill_id"])
for item in plan["agents"]:
source, target = item["source"], item["target"]
if source["prompt_template_id"] is not None and source["prompt_template_id"] not in template_ids:
raise ValueError("Legacy Agent references a missing prompt template")
expected, routing = _target_agent(source, identities, kbs, plan["model_mapping"], by_agent.get(source["id"], []))
if target != expected or item["model_resolution"] != routing or target["id"] in seen:
raise ValueError("Asset plan changes Agent identity, body, access or routing")
seen.add(target["id"])
agents[source["id"]] = target
for item in plan["agents"]:
if any(a not in agents for a in item["source"]["fusion_agent_ids"]):
raise ValueError("Legacy Agent references a missing fusion Agent")
if any(agents[a]["config"]["role"] != "production" for a in item["source"]["fusion_agent_ids"]):
raise ValueError("Legacy Agent fusion target is not a production Agent")
for binding in plan["bindings"]:
source = binding["source"]
if source["agent_id"] not in agents or binding["review"] != _binding_review(agents[source["agent_id"]], skills[source["skill_id"]]):
raise ValueError("Asset plan changes binding access review")
def safe_asset_summary(plan: dict) -> dict:
"""Return metadata only; no source body, SQL, credentials or snapshots."""
_validate_plan(plan)
return {"target_database": TARGET_DATABASE, "target_tenant_id": TARGET_TENANT,
"counts": {key: len(plan[key]) for key in ("agents", "skills", "bindings", "prompt_templates")},
"effective_bindings": sum(b["effective"] for b in plan["bindings"]),
"blocked_bindings": sum(not b["effective"] for b in plan["bindings"]),
"disabled_agent_ids": [x["source"]["id"] for x in plan["agents"] if not x["target"]["enabled"]],
"disabled_skill_ids": [x["source"]["id"] for x in plan["skills"] if not x["target"]["enabled"]],
"binding_reviews": [{"source_binding_id": b["source"]["id"], "review": b["review"]}
for b in plan["bindings"] if b["review"]],
"agent_map": {str(x["source"]["id"]): x["target"]["id"] for x in plan["agents"]},
"skill_map": {str(x["source"]["id"]): x["target"]["id"] for x in plan["skills"]}}
def asset_sql(plan: dict) -> str:
"""Return guarded SQL without executing it or adding transaction boundaries."""
_validate_plan(plan)
if not re.fullmatch(r"[a-f0-9]{64}", str(plan.get("source_snapshot_sha256", ""))):
raise ValueError("Asset SQL requires a source snapshot fingerprint")
payload = json.dumps(plan, ensure_ascii=True, sort_keys=True, separators=(",", ":"))
literal = "'" + payload.replace("'", "''") + "'::jsonb"
tag = "assets_" + hashlib.sha256(payload.encode()).hexdigest()[:16]
return f"""SET LOCAL standard_conforming_strings = on;
SET LOCAL log_statement = 'none';
SET LOCAL log_min_error_statement = 'panic';
DO ${tag}$
DECLARE
migration jsonb := {literal};
item jsonb;
target jsonb;
source_data jsonb;
mapping record;
asset_kind text;
BEGIN
IF current_database() <> '{TARGET_DATABASE}' THEN RAISE EXCEPTION 'Asset target database mismatch'; END IF;
PERFORM pg_advisory_xact_lock({TARGET_TENANT}, 11903);
PERFORM 1 FROM tenants WHERE id={TARGET_TENANT} AND status='active' AND deleted_at IS NULL FOR SHARE;
IF NOT FOUND THEN RAISE EXCEPTION 'Asset migration requires active target tenant'; END IF;
LOCK TABLE custom_agents, tenant_skill_catalog IN SHARE ROW EXCLUSIVE MODE;
CREATE TABLE IF NOT EXISTS {LEDGER} (
source_system text NOT NULL, source_organization_id bigint NOT NULL, source_kind text NOT NULL,
source_id bigint NOT NULL, target_tenant_id integer NOT NULL, target_id varchar(36) NOT NULL,
source_snapshot jsonb NOT NULL, native_snapshot jsonb NOT NULL, compatibility jsonb NOT NULL,
source_snapshot_sha256 varchar(64) NOT NULL CHECK (source_snapshot_sha256 ~ '^[a-f0-9]{{64}}$'),
created_at timestamptz NOT NULL DEFAULT now(),
PRIMARY KEY(source_system,source_organization_id,source_kind,source_id),
UNIQUE(target_tenant_id,source_kind,target_id)
);
CREATE TABLE IF NOT EXISTS {BINDING_LEDGER} (
source_system text NOT NULL, source_organization_id bigint NOT NULL, source_binding_id bigint NOT NULL,
target_tenant_id integer NOT NULL, target_agent_id varchar(36) NOT NULL, target_skill_id varchar(36) NOT NULL,
source_snapshot jsonb NOT NULL, access_review jsonb NOT NULL, effective boolean NOT NULL,
source_snapshot_sha256 varchar(64) NOT NULL CHECK (source_snapshot_sha256 ~ '^[a-f0-9]{{64}}$'),
created_at timestamptz NOT NULL DEFAULT now(),
PRIMARY KEY(source_system,source_organization_id,source_binding_id),
UNIQUE(target_tenant_id,target_agent_id,target_skill_id)
);
LOCK TABLE {LEDGER}, {BINDING_LEDGER} IN SHARE ROW EXCLUSIVE MODE;
IF EXISTS (SELECT 1 FROM jsonb_each_text(migration->'identity_mapping') i
LEFT JOIN {IDENTITY_LEDGER} m ON m.source_system='{SOURCE}' AND m.source_organization_id=1
AND m.source_user_id=i.key::bigint AND m.target_tenant_id={TARGET_TENANT} AND m.target_user_id=i.value
LEFT JOIN users u ON u.id=i.value
LEFT JOIN tenant_members tm ON tm.user_id=i.value AND tm.tenant_id={TARGET_TENANT}
WHERE m.target_user_id IS NULL OR u.id IS NULL OR u.deleted_at IS NOT NULL OR tm.id IS NULL OR tm.deleted_at IS NOT NULL)
THEN RAISE EXCEPTION 'Asset owner durable identity or membership mismatch'; END IF;
IF EXISTS (SELECT 1 FROM jsonb_each_text(migration->'kb_mapping') i
LEFT JOIN knowledge_bases k ON k.id=i.value AND k.tenant_id={TARGET_TENANT} AND k.deleted_at IS NULL
WHERE k.id IS NULL)
THEN RAISE EXCEPTION 'Asset knowledge base mapping missing or outside tenant'; END IF;
IF EXISTS (SELECT 1 FROM jsonb_array_elements(migration->'agents') AS a(value)
LEFT JOIN models m ON m.id=(a.value->'target'->'config'->>'model_id') AND m.tenant_id={TARGET_TENANT}
WHERE m.id IS NULL OR m.deleted_at IS NOT NULL OR m.status <> 'active' OR m.type <> 'KnowledgeQA')
THEN RAISE EXCEPTION 'Asset model mapping missing or not usable'; END IF;
FOREACH asset_kind IN ARRAY ARRAY['skill','agent'] LOOP
FOR item IN SELECT value FROM jsonb_array_elements(migration->(CASE asset_kind WHEN 'skill' THEN 'skills' ELSE 'agents' END)) LOOP
target := item->'target'; source_data := item->'source';
SELECT * INTO mapping FROM {LEDGER} WHERE source_system='{SOURCE}' AND source_organization_id=1
AND source_kind=asset_kind AND source_id=(source_data->>'id')::bigint;
IF FOUND THEN
IF mapping.target_tenant_id IS DISTINCT FROM {TARGET_TENANT} OR mapping.target_id IS DISTINCT FROM target->>'id'
OR mapping.source_snapshot IS DISTINCT FROM source_data
OR (mapping.native_snapshot IS DISTINCT FROM target AND NOT (
asset_kind='agent' AND NOT (mapping.native_snapshot->'config' ? 'role')
AND NOT (mapping.native_snapshot->'config' ? 'content_tags')
AND mapping.native_snapshot = (target #- '{{config,role}}' #- '{{config,content_tags}}')) AND NOT (
asset_kind='agent' AND NOT (mapping.native_snapshot->'config' ? 'fusion_agent_ids')
AND (mapping.native_snapshot = (target #- '{{config,fusion_agent_ids}}') OR (
NOT (mapping.native_snapshot->'config' ? 'role') AND NOT (mapping.native_snapshot->'config' ? 'content_tags')
AND mapping.native_snapshot = (target #- '{{config,role}}' #- '{{config,content_tags}}' #- '{{config,fusion_agent_ids}}')))))
THEN RAISE EXCEPTION 'Asset durable mapping conflict'; END IF;
-- A replay may not overwrite later edits or resurrect deleted resources.
IF asset_kind='skill' THEN
PERFORM 1 FROM tenant_skill_catalog WHERE id=target->>'id' AND tenant_id={TARGET_TENANT}
AND created_by=target->>'created_by' AND instruction_only=true;
ELSE
PERFORM 1 FROM custom_agents WHERE id=target->>'id' AND tenant_id={TARGET_TENANT} AND created_by=target->>'created_by';
END IF;
IF NOT FOUND THEN RAISE EXCEPTION 'Mapped native asset removed; refusing recreation'; END IF;
ELSE
IF asset_kind='skill' THEN
IF EXISTS (SELECT 1 FROM tenant_skill_catalog WHERE id=target->>'id'
OR (tenant_id={TARGET_TENANT} AND created_by=target->>'created_by' AND name=target->>'name' AND instruction_only=true))
THEN RAISE EXCEPTION 'Native skill ID or owner/name collision'; END IF;
INSERT INTO tenant_skill_catalog(id,tenant_id,name,description,created_by,version,instructions,instruction_only,
visibility,enabled,bundle_ref,bundle_sha256,created_at,updated_at)
VALUES(target->>'id',{TARGET_TENANT},target->>'name',target->>'description',target->>'created_by',target->>'version',
target->>'instructions',true,target->>'visibility',(target->>'enabled')::boolean,'',target->>'bundle_sha256',now(),now());
ELSE
IF EXISTS (SELECT 1 FROM custom_agents WHERE id=target->>'id'
OR (tenant_id={TARGET_TENANT} AND created_by=target->>'created_by' AND name=target->>'name'))
THEN RAISE EXCEPTION 'Native Agent ID or owner/name collision'; END IF;
INSERT INTO custom_agents(id,tenant_id,name,description,created_by,is_builtin,visibility,enabled,config,created_at,updated_at)
VALUES(target->>'id',{TARGET_TENANT},target->>'name',target->>'description',target->>'created_by',false,
target->>'visibility',(target->>'enabled')::boolean,target->'config',now(),now());
END IF;
INSERT INTO {LEDGER}(source_system,source_organization_id,source_kind,source_id,target_tenant_id,target_id,
source_snapshot,native_snapshot,compatibility,source_snapshot_sha256)
VALUES('{SOURCE}',1,asset_kind,(source_data->>'id')::bigint,{TARGET_TENANT},target->>'id',source_data,target,
jsonb_build_object('unsupported_fields',migration->'unsupported_fields','model_resolution',item->'model_resolution',
'access_policy',migration->'access_policy','execution',item->'compatibility'),migration->>'source_snapshot_sha256');
END IF;
END LOOP;
END LOOP;
FOR item IN SELECT value FROM jsonb_array_elements(migration->'prompt_templates') LOOP
SELECT * INTO mapping FROM {LEDGER} WHERE source_system='{SOURCE}' AND source_organization_id=1
AND source_kind='template' AND source_id=(item->>'id')::bigint;
IF FOUND THEN
IF mapping.target_tenant_id <> {TARGET_TENANT} OR mapping.source_snapshot IS DISTINCT FROM item
THEN RAISE EXCEPTION 'Legacy prompt template provenance conflict'; END IF;
ELSE
INSERT INTO {LEDGER}(source_system,source_organization_id,source_kind,source_id,target_tenant_id,target_id,
source_snapshot,native_snapshot,compatibility,source_snapshot_sha256)
VALUES('{SOURCE}',1,'template',(item->>'id')::bigint,{TARGET_TENANT},'legacy-template-' || (item->>'id'),item,'{{}}',
'{{"execution":"ledger_only_legacy_template_format_not_evaluated"}}',migration->>'source_snapshot_sha256');
END IF;
END LOOP;
FOR item IN SELECT value FROM jsonb_array_elements(migration->'bindings') LOOP
SELECT * INTO mapping FROM {BINDING_LEDGER} WHERE source_system='{SOURCE}' AND source_organization_id=1
AND source_binding_id=(item->'source'->>'id')::bigint;
IF FOUND THEN
IF mapping.target_tenant_id <> {TARGET_TENANT} OR mapping.target_agent_id IS DISTINCT FROM item->>'agent_id'
OR mapping.target_skill_id IS DISTINCT FROM item->>'skill_id' OR mapping.source_snapshot IS DISTINCT FROM item->'source'
OR mapping.access_review IS DISTINCT FROM item->'review' OR mapping.effective IS DISTINCT FROM (item->>'effective')::boolean
THEN RAISE EXCEPTION 'Legacy skill binding provenance conflict'; END IF;
ELSE
INSERT INTO {BINDING_LEDGER}(source_system,source_organization_id,source_binding_id,target_tenant_id,target_agent_id,
target_skill_id,source_snapshot,access_review,effective,source_snapshot_sha256)
VALUES('{SOURCE}',1,(item->'source'->>'id')::bigint,{TARGET_TENANT},item->>'agent_id',item->>'skill_id',
item->'source',item->'review',(item->>'effective')::boolean,migration->>'source_snapshot_sha256');
END IF;
END LOOP;
END ${tag}$;
"""