Skip to content

Artifact heal 自動修復在唯一候選檔案已被其他 artifact 佔用時,靜默把兩個 artifact 接到同一份檔案 #116

Description

@MarkTsaiCqi

摘要

Artifact 的「heal」自動修復流程(用來處理指標損壞的 pointer,見 POST /{agent_id}/artifacts/{artifact_id}/heal)在「workspace 裡剛好只掃到一個同副檔名候選檔案」時,會不經任何確認、靜默地把損壞的 artifact 重新註冊到那個候選檔案上——即使那個檔案已經是另一個 artifact 的 entry file。結果是兩個完全不同的 artifact 記錄(不同 artifact_id、不同標題、不同建立時間)指向同一份實體檔案,其中一個的標題/身份跟它實際顯示的內容完全對不上。

重現步驟(實測紀錄)

環境:https://dev-agent.narra.nexus,xyzdev01,Nova(agent_4bf9d757910b

  1. 請 Nova 建立一個新的 HTML artifact:art_bb19e14e,標題「QA-ARTIFACT02-heal-test」,entry file 為 qa_artifact02/index.html
  2. 用 Bash 只刪除這一個實體檔案(rm qa_artifact02/index.html),DB 裡的 artifact 記錄不動——模擬「pointer 損壞」情境
  3. 存取這個 artifact 的 raw 端點(GET /api/public/artifacts/raw/{token}/)→ 正確回 410 {"detail":"artifact file missing on disk"},符合設計預期
  4. 重新整理 artifact 面板(觸發前端自動呼叫 heal)→ 再次存取同一個 artifact,變成 HTTP 200,但回傳內容是 Nova 原本就有的「Welcome to NarraNexus」歡迎頁(last-modified: 2026-06-18),完全不是這次測試建立的內容
  5. 請 Nova 查詢目前的 artifact registry,確認:
    art_bb19e14e  [text/html]  "QA-ARTIFACT02-heal-test"   → welcome/index.html
    art_5196fa1b  [text/html]  "Welcome to NarraNexus"     → welcome/index.html
    
    兩個不同的 artifact_id、不同標題,現在指向同一個實體檔案qa_artifact02/ 目錄確認為空(total 0),workspace 裡確認只剩這一個 .html 檔案。

程式碼層根因

xyz_agent_context/artifact/_artifact_impl/heal.pyheal_artifact()

candidates = _scan_workspace_for_kind(workspace_root, art.kind)
if len(candidates) == 1:
    only = candidates[0]
    result = await registration.register_artifact(
        repo=repo, agent_id=agent_id, user_id=user_id, session_id=None,
        kind=art.kind, entry_path=only.workspace_path,
        title=art.title, description=art.description,
        target_artifact_id=artifact_id,
    )
    ...
    return HealResult(recovered=True, artifact=healed, message=f"auto-recovered from {only.workspace_path}")

_scan_workspace_for_kind()單純用副檔名掃 workspace 底下所有檔案,完全沒有排除「已經被其他 artifact_id 佔用當 entry file」的檔案。當掃描結果剛好只有 1 個候選(很容易發生——例如這次測試中,刪除唯一的測試檔案後,workspace 裡就只剩 Nova 原本的 welcome/index.html),會直接判定為「唯一解」、自動註冊、recovered=True,全程沒有跳出候選清單給使用者確認的機會(那個分支只在候選數 > 1 時才會出現)。

影響

  • 資料完整性/使用者困惑:使用者看到的 artifact 標題還是「QA-ARTIFACT02-heal-test」,但點開內容卻是別的東西(這次是無害的歡迎頁,但原則上可能是任何其他 artifact 的真實內容)
  • 靜默、不可逆:整個過程沒有任何警告或確認 UI,使用者不會意識到自己的 artifact 已經被接到別人(或自己另一個)的檔案上
  • 兩個 artifact 記錄共享同一個實體檔案後,其中一邊之後如果被編輯或(未來若支援)刪除底層檔案,會連帶影響另一個看似無關的 artifact
  • severity 判定為中:不涉及跨使用者/跨帳號(同一個 agent 自己的 workspace 內),但資料完整性層面是實打實地把 A 的身份接到 B 的內容上

建議修復方向

  • _scan_workspace_for_kind()heal_artifact() 的候選篩選階段,應該排除已經被其他 artifact_id 註冊為 entry file 的路徑(跟 ArtifactRepository 查一次即可)
  • 或者,即使候選數剛好是 1,只要那個候選已被其他 artifact 佔用,就不要走「自動修復」快速路徑,改成跟「多候選」一樣的行為——列出來讓使用者確認,並附上警告「這個檔案已經是另一個 artifact(xxx)在用」

測試環境

  • https://dev-agent.narra.nexus
  • 程式碼:src/xyz_agent_context/artifact/_artifact_impl/heal.pyheal_artifact_scan_workspace_for_kind)、backend/routes/agents/artifacts.pyheal 端點、delete_artifact 對照——後者有正確做到「只刪 DB row、不動檔案」)
  • 對應測試計畫項目:qa/features/collaboration-marketplace/test-plan-collaboration-marketplace.md TC-ARTIFACT-02
  • 測試殘留已清理:透過 UI 刪除了 art_bb19e14e 的 registry 記錄(僅刪除 artifact 記錄本身,未影響 welcome/index.html 實體檔案,也未影響 Nova 原本的 art_5196fa1b

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions