mirror of
https://github.com/multica-ai/multica.git
synced 2026-06-16 19:29:26 +02:00
Codex Desktop writes one [[skills.config]] entry per known skill into ~/.codex/config.toml. File-backed entries get path = "...", but plugin-backed entries (e.g. name = "superpowers:brainstorming") only get a name. Codex CLI 0.114's TOML deserializer treats path as required, so it rejects the plugin entries with "missing field path" and fails thread/start. The daemon copies ~/.codex/config.toml verbatim into each task's isolated codex-home, which propagated those broken entries into the per-task config and blocked every Codex agent run for affected users. Strip the whole [[skills.config]] array on copy. Multica writes the agent's currently assigned skills directly to codex-home/skills/ and Codex auto-discovers them from there, so the user-level skill registry is redundant for a per-task run. Closes #1753
88 lines
2.8 KiB
Go
88 lines
2.8 KiB
Go
package execenv
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
)
|
|
|
|
// stripSkillsConfigEntries removes every `[[skills.config]]` array-of-tables
|
|
// block from the given config.toml content.
|
|
//
|
|
// Background: Codex Desktop writes one `[[skills.config]]` entry per skill it
|
|
// knows about — file-backed skills get a `path = "..."` field, while
|
|
// plugin-backed skills (e.g. `name = "superpowers:brainstorming"`) only get a
|
|
// `name`. Codex CLI 0.114's TOML deserializer treats `path` as a required
|
|
// field, so it rejects the plugin entries with `missing field path` and
|
|
// refuses to start. Multica copies the user's `~/.codex/config.toml` verbatim
|
|
// into each task's isolated codex-home, which propagates the broken entries
|
|
// into the per-task config and blocks `codex thread/start`.
|
|
//
|
|
// Stripping the whole `[[skills.config]]` array sidesteps the issue: Multica
|
|
// writes the agent's currently assigned skills directly to
|
|
// `codex-home/skills/<name>/SKILL.md`, and Codex auto-discovers them from
|
|
// that directory. The user-level skill registry is irrelevant to a per-task
|
|
// run, so dropping it is both safe and the right scope of isolation.
|
|
//
|
|
// Lines outside `[[skills.config]]` blocks are preserved untouched.
|
|
func stripSkillsConfigEntries(content string) string {
|
|
if !strings.Contains(content, "[[skills.config]]") {
|
|
return content
|
|
}
|
|
|
|
lines := strings.Split(content, "\n")
|
|
out := make([]string, 0, len(lines))
|
|
inSkillsConfig := false
|
|
for _, line := range lines {
|
|
trimmed := strings.TrimSpace(line)
|
|
|
|
// A new TOML header always closes the current `[[skills.config]]`
|
|
// block, regardless of whether it's another entry of the same array
|
|
// or a different table.
|
|
if strings.HasPrefix(trimmed, "[") {
|
|
if trimmed == "[[skills.config]]" {
|
|
inSkillsConfig = true
|
|
continue
|
|
}
|
|
inSkillsConfig = false
|
|
out = append(out, line)
|
|
continue
|
|
}
|
|
|
|
if inSkillsConfig {
|
|
continue
|
|
}
|
|
out = append(out, line)
|
|
}
|
|
|
|
stripped := strings.Join(out, "\n")
|
|
// Collapse the trailing blank-line cluster that the removal can leave
|
|
// behind so repeated copies don't grow the file unboundedly.
|
|
stripped = strings.TrimRight(stripped, "\n") + "\n"
|
|
if strings.TrimSpace(stripped) == "" {
|
|
return ""
|
|
}
|
|
return stripped
|
|
}
|
|
|
|
// sanitizeCopiedCodexConfig rewrites the per-task config.toml in place,
|
|
// dropping `[[skills.config]]` entries inherited from the shared
|
|
// `~/.codex/config.toml`. No-op if the file doesn't exist or doesn't change.
|
|
func sanitizeCopiedCodexConfig(configPath string) error {
|
|
data, err := os.ReadFile(configPath)
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
return nil
|
|
}
|
|
return fmt.Errorf("read config.toml: %w", err)
|
|
}
|
|
stripped := stripSkillsConfigEntries(string(data))
|
|
if stripped == string(data) {
|
|
return nil
|
|
}
|
|
if err := os.WriteFile(configPath, []byte(stripped), 0o644); err != nil {
|
|
return fmt.Errorf("write config.toml: %w", err)
|
|
}
|
|
return nil
|
|
}
|