Files
SivanCola 4fe345ec44 feat(studio): bind immutable Agent compositions to sessions
Problem: Studio persona settings cannot select an independent tool and module
combination for each conversation, or preserve that combination across reloads.

Root cause: discovery and runtime construction follow mutable shared configuration
without a versioned session composition or a shared admission boundary.

Fix: introduce versioned scoped definitions, retained resources and authoritative
session bindings. Assemble isolated runtime selections, enforce tool ceilings,
pin MCP contracts, and share the lifecycle gate with first-prompt admission.
Expose independent HTTP, CLI, ACP and Studio management and selection controls,
with migration, native import/export, diagnostics and read-only history recovery.

Verification: composition mechanism tests, provider-wire cache tests, lifecycle
rollback tests, sensitive-boundary regressions, frontend typecheck and all 5,219
frontend tests; gofmt, vet, lint and repository checks. Browser light/dark wide
and 390px screenshots and real Electron backend interactions are included.

Refs: #13588
2026-10-17 19:09:49 +08:00

71 lines
2.0 KiB
Go

package boot
import (
"fmt"
"maps"
"path/filepath"
"slices"
"strings"
"reasonix/internal/contract/agentcomposition"
"reasonix/internal/contract/config"
"reasonix/internal/ext/pluginpkg"
"reasonix/internal/state/agentpreset"
)
func rebaseCompositionPath(value, from, to string) string {
prefix := ""
if key, path, ok := strings.Cut(value, "="); ok && strings.HasPrefix(key, "--") {
prefix, value = key+"=", path
}
if !filepath.IsAbs(value) {
return prefix + value
}
rel, err := filepath.Rel(from, value)
if err != nil || !filepath.IsLocal(rel) {
return prefix + value
}
return prefix + filepath.Join(to, rel)
}
func (b *builder) rebaseCompositionMCP(entry config.PluginEntry) (config.PluginEntry, error) {
owner, owned := b.cfg.PluginPackageOwner(entry.Name)
if !owned {
return entry, nil
}
if !member(b.opts.Composition.Resolved.Extensions, owner) {
return entry, fmt.Errorf("%w: MCP %s requires extension %s", agentcomposition.ErrUnavailable, entry.Name, owner)
}
installed, _ := pluginpkg.LoadInstalled(b.roots.Home())
retained, err := resource[retainedExtension](b.opts.Composition, "extension")
if err != nil {
return entry, err
}
for _, saved := range retained {
if saved.Package.Installed.Name != owner {
continue
}
for _, live := range installed {
if live.Installed.Name != owner {
continue
}
root, err := agentpreset.New(b.roots.Home(), b.root).Materialize(saved.Assets)
if err != nil {
return entry, err
}
entry.Args = slices.Clone(entry.Args)
entry.Env = maps.Clone(entry.Env)
entry.Source = config.MCPSourcePluginPackage
entry.Command = rebaseCompositionPath(entry.Command, live.Package.Root, root)
for i, arg := range entry.Args {
entry.Args[i] = rebaseCompositionPath(arg, live.Package.Root, root)
}
for key, value := range entry.Env {
entry.Env[key] = rebaseCompositionPath(value, live.Package.Root, root)
}
return entry, nil
}
}
return entry, fmt.Errorf("%w: MCP package resource missing", agentcomposition.ErrUnavailable)
}