fix(skills): fix dep detection false positives and all-or-nothing install

- Filter Python stdlib modules at scan time to prevent false positives
  when the runtime checker fails (e.g. pip:argparse, pip:sys)
- Install pip/npm packages one-by-one instead of batch so partial
  success is preserved when one package fails
- Persist missing deps to DB after install via StoreMissingDeps() so
  reload reflects actual state instead of stale data
- Use explicit master tenant context in handleInstallDeps for
  consistency with rescanAndUpdate()
This commit is contained in:
viettranx committed 2026-04-02 11:42:14 +07:00
1 parent ecf6463d98
commit 899c8eb0b4
5 files changed
+330 -25

No files matched your search

+26 -10
View File
@@ -255,7 +255,11 @@ func (h *SkillsHandler) handleInstallDeps(w http.ResponseWriter, r *http.Request
if !h.requireMasterTenant(w, r) {
return
}
dirs := h.skills.ListSystemSkillDirs(r.Context())
// Use explicit master tenant context for system skill operations,
// consistent with rescanAndUpdate() pattern.
masterCtx := store.WithTenantID(r.Context(), store.MasterTenantID)
dirs := h.skills.ListSystemSkillDirs(masterCtx)
if len(dirs) == 0 {
writeJSON(w, http.StatusOK, map[string]string{"message": "no system skills"})
return
@@ -274,25 +278,24 @@ func (h *SkillsHandler) handleInstallDeps(w http.ResponseWriter, r *http.Request
})
}
result, err := installManagedDeps(r.Context(), manifest, missing)
result, err := installManagedDeps(masterCtx, manifest, missing)
if err != nil {
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
// Re-check all system skills and update status after install
allSkills := h.skills.ListAllSkills(r.Context())
// Re-check all system skills, persist missing deps, and update status.
allSkills := h.skills.ListAllSkills(masterCtx)
statusChanged := false
for _, sk := range allSkills {
if !sk.IsSystem {
continue
}
dir, exists := dirs[sk.Slug]
if !exists {
if _, exists := dirs[sk.Slug]; !exists {
continue
}
m := h.scanWithFallback(sk)
if m == nil || m.IsEmpty() {
_ = dir // dir was used for direct scan; fallback uses sk.BaseDir
continue
}
ok, miss := skills.CheckSkillDeps(m)
@@ -300,10 +303,20 @@ func (h *SkillsHandler) handleInstallDeps(w http.ResponseWriter, r *http.Request
if err != nil {
continue
}
if ok && sk.Status == "archived" {
_ = h.skills.UpdateSkill(r.Context(), id, map[string]any{"status": "active"})
h.skills.BumpVersion()
// Persist actual missing deps to DB so reload reflects reality.
_ = h.skills.StoreMissingDeps(masterCtx, id, miss)
// Update status in both directions.
switch {
case ok && sk.Status == "archived":
_ = h.skills.UpdateSkill(masterCtx, id, map[string]any{"status": "active"})
statusChanged = true
case !ok && sk.Status != "archived":
_ = h.skills.UpdateSkill(masterCtx, id, map[string]any{"status": "archived"})
statusChanged = true
}
status := "active"
if !ok {
status = "archived"
@@ -319,6 +332,9 @@ func (h *SkillsHandler) handleInstallDeps(w http.ResponseWriter, r *http.Request
})
}
}
if statusChanged {
h.skills.BumpVersion()
}
if h.msgBus != nil {
h.msgBus.Broadcast(bus.Event{