Files
SivanCola eb2c2851d1 fix(composition): retain linked resources and explain dependency failures
Problem:
Saved personas can fail when selected with inherited skills, extensions, or
hooks. MCP startup failures lack structured server identity and guidance.

Root cause:
Ordinary discovery accepts linked sources but composition snapshot capture
rejects them. MCP startup errors carry the server only in prose instead of
the module metadata consumed by the localized refusal boundary.

Fix:
Flatten contained source links through rooted reads, retain strict snapshot
verification, and preserve module identities and sanitized failure causes
through HTTP and Chinese/English rendering. Keep required MCP failures
blocking and preserve the original session on a failed selection.

Verification:
Full root Go tests, go vet, make lint, make check, frontend typecheck, and
198 relevant frontend tests pass. Browser fixtures cover failure, retry,
management access, and light/dark wide/phone error presentation. Regressions
cover source deletion and restart, prefix stability, revocation, containment,
snapshot tampering, and credential redaction.
2026-10-20 16:03:09 +08:00

61 lines
1.7 KiB
Go

package boot
import (
"encoding/json"
"os"
"path/filepath"
"reasonix/internal/contract/agentcomposition"
"reasonix/internal/ext/skill"
"reasonix/internal/state/agentpreset"
)
func (b *builder) retainSkill(sk skill.Skill) (skill.Skill, error) {
if sk.Scope == skill.ScopeBuiltin || sk.Path == "" || sk.Path == "(builtin)" {
return sk.Complete(), nil
}
var asset agentpreset.DirectorySnapshot
var err error
if filepath.Base(sk.Path) == skill.SkillFile {
asset, err = agentpreset.CaptureDirectory(filepath.Dir(sk.Path))
} else {
var data []byte
data, err = os.ReadFile(sk.Path)
asset.Files = []agentpreset.AssetFile{{Path: filepath.Base(sk.Path), Data: data, Mode: 0o600}}
}
if err != nil {
return sk, err
}
if err := keepResource(b.opts.Composition, "skill-assets", sk.SlashName(), asset); err != nil {
return sk, err
}
root, err := agentpreset.New(b.roots.Home(), b.root).Materialize(asset)
if err != nil {
return sk, err
}
sk.Path = filepath.Join(root, filepath.Base(sk.Path))
return sk.Complete(), nil
}
func (b *builder) restoreSkills(skills []skill.Skill) error {
for i := range skills {
for _, r := range b.opts.Composition.Resources {
if r.Kind != "skill-assets" || r.ID != skills[i].SlashName() {
continue
}
var asset agentpreset.DirectorySnapshot
if err := jsonResource(r, &asset); err != nil {
return err
}
root, err := agentpreset.New(b.roots.Home(), b.root).Materialize(asset)
if err != nil {
return compositionResourceError("skill", skills[i].SlashName(), err)
}
skills[i].Path = filepath.Join(root, filepath.Base(skills[i].Path))
}
}
return nil
}
func jsonResource(r agentcomposition.Resource, v any) error { return json.Unmarshal(r.Data, v) }