From 79a681b88b1fe93dd4fcfcf2919dffa44bb3da9b Mon Sep 17 00:00:00 2001 From: Jyong Date: Tue, 28 Jul 2026 06:08:21 -0400 Subject: [PATCH] fix(knowledge-fs): persist greenfield audit anchor --- api/services/knowledge_fs/cutover.py | 51 ++++++++++++++++--- .../services/test_knowledge_fs_cutover.py | 19 +++++++ 2 files changed, 62 insertions(+), 8 deletions(-) diff --git a/api/services/knowledge_fs/cutover.py b/api/services/knowledge_fs/cutover.py index 4f028da2d62..9eb3e94489d 100644 --- a/api/services/knowledge_fs/cutover.py +++ b/api/services/knowledge_fs/cutover.py @@ -2236,17 +2236,48 @@ class KnowledgeFSWorkspaceCutoverService: return _ActivationAnchor(control_space.id, False) greenfield_anchor = _greenfield_activation_anchor_id(tenant_id) - has_local_control_space = session.scalar( - sa.select(sa.literal(True)) + greenfield_provisioning_key = _greenfield_activation_provisioning_key(tenant_id) + local_control_space_count = session.scalar( + sa.select(sa.func.count()) .select_from(KnowledgeFSControlSpace) .where(KnowledgeFSControlSpace.tenant_id == tenant_id) - .limit(1) ) - if ( - not has_local_control_space - and _is_zero_space_cutover_ledger(ledger) - and control_space_id in {None, greenfield_anchor} - ): + if not _is_zero_space_cutover_ledger(ledger) or control_space_id not in {None, greenfield_anchor}: + raise KnowledgeFSCutoverGateBlockedError( + "A tenant-owned active control-space is required for activation audit" + ) + + persisted_anchor = session.scalar( + sa.select(KnowledgeFSControlSpace).where( + KnowledgeFSControlSpace.tenant_id == tenant_id, + KnowledgeFSControlSpace.id == greenfield_anchor, + ) + ) + if persisted_anchor is not None: + if ( + local_control_space_count == 1 + and persisted_anchor.state is KnowledgeFSControlSpaceState.DELETED + and persisted_anchor.knowledge_space_id is None + and persisted_anchor.provisioning_key == greenfield_provisioning_key + and persisted_anchor.owner_account_id == ledger.shadow_completed_by_account_id + and persisted_anchor.lifecycle_operation_id == greenfield_anchor + ): + return _ActivationAnchor(greenfield_anchor, True) + raise KnowledgeFSCutoverGateBlockedError( + "KnowledgeFS greenfield activation audit anchor conflicts with local control-space state" + ) + + if local_control_space_count == 0 and ledger.shadow_completed_by_account_id is not None: + persisted_anchor = KnowledgeFSControlSpace( + tenant_id=tenant_id, + owner_account_id=ledger.shadow_completed_by_account_id, + provisioning_key=greenfield_provisioning_key, + state=KnowledgeFSControlSpaceState.DELETED, + lifecycle_operation_id=greenfield_anchor, + ) + persisted_anchor.id = greenfield_anchor + session.add(persisted_anchor) + session.flush() return _ActivationAnchor(greenfield_anchor, True) raise KnowledgeFSCutoverGateBlockedError("A tenant-owned active control-space is required for activation audit") @@ -2364,6 +2395,10 @@ def _greenfield_activation_anchor_id(tenant_id: str) -> str: return str(uuid5(NAMESPACE_URL, f"dify-kfs-greenfield-activation:{tenant_id}")) +def _greenfield_activation_provisioning_key(tenant_id: str) -> str: + return f"dify-kfs-greenfield-activation:{tenant_id}" + + def _is_zero_space_cutover_ledger(ledger: KnowledgeFSWorkspaceCutoverLedger) -> bool: return bool( ledger.source_revision_watermark == _ZERO_REVISION_WATERMARK diff --git a/api/tests/unit_tests/services/test_knowledge_fs_cutover.py b/api/tests/unit_tests/services/test_knowledge_fs_cutover.py index 2e8f431eb17..ba9f52dbf0e 100644 --- a/api/tests/unit_tests/services/test_knowledge_fs_cutover.py +++ b/api/tests/unit_tests/services/test_knowledge_fs_cutover.py @@ -464,6 +464,19 @@ def test_empty_workspace_uses_a_stable_greenfield_anchor_for_remote_freeze_and_a assert UUID(remote.freeze_requests[0].control_space_id) assert ledger.phase is KnowledgeFSWorkspaceCutoverPhase.CUTOVER assert ledger.product_routes_enabled is True + with session_maker() as session: + audit_anchor = session.scalar( + sa.select(KnowledgeFSControlSpace).where( + KnowledgeFSControlSpace.tenant_id == tenant_id, + KnowledgeFSControlSpace.id == remote.freeze_requests[0].control_space_id, + ) + ) + assert audit_anchor is not None + assert audit_anchor.state is KnowledgeFSControlSpaceState.DELETED + assert audit_anchor.knowledge_space_id is None + assert audit_anchor.owner_account_id == str( + service.status(tenant_id=tenant_id)["shadow_completed_by_account_id"] + ) def test_empty_workspace_freeze_rejects_a_nonempty_remote_namespace( @@ -506,6 +519,12 @@ def test_empty_workspace_freeze_rejects_a_nonempty_remote_namespace( freeze_at=_BASE_TIME + timedelta(minutes=1), ) assert remote.freeze_requests == [] + with session_maker() as session: + audit_anchor = session.scalar( + sa.select(KnowledgeFSControlSpace).where(KnowledgeFSControlSpace.tenant_id == tenant_id) + ) + assert audit_anchor is not None + assert audit_anchor.state is KnowledgeFSControlSpaceState.DELETED def _quarantine_resolution(