release: v0.6.0 — security audit fixes + 7 new features
All checks were successful
PR Check / check (pull_request) Successful in 57s
All checks were successful
PR Check / check (pull_request) Successful in 57s
Audit corrections (security, concurrency, stability): - chat_engine: bound resp.Choices[0] access, release tool slot per-iteration - conversation_multi: synchronous save under existing lock (was racy fire-and-forget) - workflow/engine: short-circuit on failed deps (no more infinite busy-wait); track failed/skipped status - handlers_workflow: rune-aware truncate for plan goal (UTF-8 safe) - server: CORS limited to localhost origins (was wildcard) - handlers_info / terminal: mask API keys and SSH passwords as "***" in GET responses; preserve stored secret if "***" sent on update - terminal: sshpass uses -e + SSHPASS env var (was both -p and -e) - handlers_chat: MaxBytesReader 50 MB on /api/chat - image_cache: 10 MB cap per image - handlers_config: font size <= 72; profile-save unmarshal errors propagated - handlers_info: /lsp/auto-install ProjectDir restricted to user home - Shell.jsx: parenthesized resize-condition (operator precedence) - orchestrator_test: CleanAIResponse capitalization (fixes failing vet) New features: - platform: detect OS name (Debian, Ubuntu, Windows 11, macOS X.Y) and inject in Studio system prompt next to the date - agents: default timeout 30 min for crush_run/claude_run (cap also 30 min) - agents: new cwd, wsl_distro, wsl_user params; on Windows hosts launch via "wsl -d <distro> -u <user> --cd <cwd> --" - agents: new claude_run tool (mirror of crush_run for Claude Code CLI) - terminal: list installed WSL distros individually in new-tab menu (Windows only) - studio: system prompt rewritten around BMAD-METHOD personas + mandatory delegation template - studio: "Réflexion avancée" toggle — inactive provider produces a preliminary report injected as [RAPPORT PRÉALABLE] context for the active provider - studio: "Historique compressé" toggle — collapses past tool calls to last action only, with "Tout afficher" expansion
This commit is contained in:
@@ -142,6 +142,37 @@ func New(cfg *config.MuyueConfig) (*Orchestrator, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
// NewForProvider builds an orchestrator using a specific (non-active) provider,
|
||||
// for the Advanced Reflection feature where the inactive provider produces a
|
||||
// preliminary report before the active provider answers. Excludes the currently
|
||||
// active provider from selection — picks the first other configured provider
|
||||
// with a non-empty API key.
|
||||
func NewForInactiveProvider(cfg *config.MuyueConfig) (*Orchestrator, error) {
|
||||
var activeName string
|
||||
for _, p := range cfg.AI.Providers {
|
||||
if p.Active {
|
||||
activeName = p.Name
|
||||
break
|
||||
}
|
||||
}
|
||||
for i := range cfg.AI.Providers {
|
||||
p := &cfg.AI.Providers[i]
|
||||
if p.Name == activeName {
|
||||
continue
|
||||
}
|
||||
if p.APIKey == "" {
|
||||
continue
|
||||
}
|
||||
return &Orchestrator{
|
||||
config: cfg,
|
||||
provider: p,
|
||||
client: sharedHTTPClient,
|
||||
history: []Message{},
|
||||
}, nil
|
||||
}
|
||||
return nil, fmt.Errorf("no inactive provider with API key configured")
|
||||
}
|
||||
|
||||
func (o *Orchestrator) SetSystemPrompt(prompt string) {
|
||||
o.systemPrompt = prompt
|
||||
}
|
||||
@@ -174,6 +205,33 @@ func (o *Orchestrator) GetHistory() []Message {
|
||||
return out
|
||||
}
|
||||
|
||||
// SendNoTools issues a one-shot, history-less request to this orchestrator's
|
||||
// provider. Used by the Advanced Reflection feature so the inactive provider
|
||||
// can produce a preliminary report without contaminating the active
|
||||
// orchestrator's history or invoking tools.
|
||||
func (o *Orchestrator) SendNoTools(userMessage string) (string, error) {
|
||||
messages := make([]Message, 0, 2)
|
||||
if o.systemPrompt != "" {
|
||||
messages = append(messages, Message{Role: "system", Content: TextContent(o.systemPrompt)})
|
||||
}
|
||||
messages = append(messages, Message{Role: "user", Content: TextContent(userMessage)})
|
||||
|
||||
reqBody := ChatRequest{
|
||||
Model: o.provider.Model,
|
||||
Messages: messages,
|
||||
Stream: false,
|
||||
}
|
||||
|
||||
chatResp, _, err := o.sendWithFallback(reqBody, "")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if len(chatResp.Choices) == 0 {
|
||||
return "", fmt.Errorf("empty response from provider")
|
||||
}
|
||||
return CleanAIResponse(chatResp.Choices[0].Message.Content), nil
|
||||
}
|
||||
|
||||
func (o *Orchestrator) Send(userMessage string) (string, error) {
|
||||
o.histMu.Lock()
|
||||
o.history = append(o.history, Message{
|
||||
|
||||
@@ -65,11 +65,11 @@ func TestCleanAIResponse(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := cleanAIResponse(tt.input)
|
||||
result := CleanAIResponse(tt.input)
|
||||
result = strings.TrimSpace(result)
|
||||
expected := strings.TrimSpace(tt.expected)
|
||||
if result != expected {
|
||||
t.Errorf("cleanAIResponse(%q) = %q, want %q", tt.input, result, expected)
|
||||
t.Errorf("CleanAIResponse(%q) = %q, want %q", tt.input, result, expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -77,34 +77,34 @@ func TestCleanAIResponse(t *testing.T) {
|
||||
|
||||
func TestCleanAIResponseThinkRegex(t *testing.T) {
|
||||
input2 := "<Think>some reasoning</Think>actual response"
|
||||
result2 := cleanAIResponse(input2)
|
||||
result2 := CleanAIResponse(input2)
|
||||
if result2 != "actual response" {
|
||||
t.Errorf("Valid Think tags should be removed: %q", result2)
|
||||
}
|
||||
|
||||
input3 := "<think\nmultiline\nreasoning</think visible"
|
||||
result3 := cleanAIResponse(input3)
|
||||
result3 := CleanAIResponse(input3)
|
||||
// No closing > on opening tag, so won't match regex
|
||||
if result3 != "<think\nmultiline\nreasoning</think visible" {
|
||||
t.Errorf("Malformed think should not be removed: %q", result3)
|
||||
}
|
||||
|
||||
input4 := "<think type=re>reasoning</think visible"
|
||||
result4 := cleanAIResponse(input4)
|
||||
result4 := CleanAIResponse(input4)
|
||||
// </think followed by space, not >, so won't match
|
||||
if result4 != "<think type=re>reasoning</think visible" {
|
||||
t.Errorf("Malformed closing should not be removed: %q", result4)
|
||||
}
|
||||
|
||||
input_real := "prefix<think reasoning here</think suffix"
|
||||
result_real := cleanAIResponse(input_real)
|
||||
result_real := CleanAIResponse(input_real)
|
||||
// The closing </think has no > after it, so won't match
|
||||
if result_real != "prefix<think reasoning here</think suffix" {
|
||||
t.Errorf("Malformed tags should pass through: %q", result_real)
|
||||
}
|
||||
|
||||
input_valid := "<Think>reasoning</Think>result"
|
||||
result_valid := cleanAIResponse(input_valid)
|
||||
result_valid := CleanAIResponse(input_valid)
|
||||
if result_valid != "result" {
|
||||
t.Errorf("Valid tags should be removed: %q", result_valid)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user