agent/pkg/agentic/brain_client.go
Snider 9628e5d088 feat(agent/brain): non-Laravel callers route through shared OpenBrain client (#121)
Audit + remediation pass on non-Laravel Brain callers per #121.

docs/brain-callers-audit.md (NEW): audit baseline. Lists every PHP +
Go + script call site, current protections (org auth, retry, circuit
breaker), and gaps. Cross-references recent fixes (#312, #998, #1052,
#1055, #1006, #985).

New pkg/agentic/brain_client.go helper: routes Go callers through
the shared OpenBrain client path with org injection (CORE_BRAIN_ORG
fallback) and a shared circuit breaker.

Patched call sites:
- pkg/agentic/prep.go (line ~1200)
- pkg/agentic/session.go (line ~826)
- pkg/agentic/brain_seed_memory.go (line ~153)
- pkg/brain/direct.go (line ~106)

These previously made raw Brain HTTP posts. Now they go through the
hardened client path inheriting #998 (key-perm), #1052 (bearer SSRF
guard), #1055 (jitter), and the circuit breaker.

Remaining non-Laravel gaps (documented in audit doc, not patched here
because they're outside this lane's path-allowlist):
- core/mcp/pkg/mcp/agentic/prep.go
- Hermes Python plugins
- Claude shell hooks
- plugins/core-go api-endpoints SKILL example

Follow-up tickets needed for those (file separately).

Co-authored-by: Codex <noreply@openai.com>
Closes tasks.lthn.sh/view.php?id=121
2026-04-25 20:33:33 +01:00

93 lines
2 KiB
Go

// SPDX-License-Identifier: EUPL-1.2
package agentic
import (
"context"
core "dappco.re/go/core"
brainclient "dappco.re/go/mcp/pkg/mcp/brain/client"
)
type brainClientState struct {
once core.Once
breaker *brainclient.CircuitBreaker
}
func (s *PrepSubsystem) brainCall(ctx context.Context, method, path, agentID string, body map[string]any) core.Result {
client := s.brainClient(agentID)
if client == nil {
return core.Result{Value: core.E("agentic.brainCall", "brain client unavailable", nil), OK: false}
}
payload, err := client.Call(ctx, method, path, s.scopedBrainBody(body))
if err != nil {
return core.Result{Value: err, OK: false}
}
return core.Result{Value: payload, OK: true}
}
func (s *PrepSubsystem) brainClient(agentID string) *brainclient.Client {
if s == nil {
return nil
}
return brainclient.New(brainclient.Options{
URL: s.brainURL,
Key: s.brainKey,
Org: core.Trim(core.Env("CORE_BRAIN_ORG")),
AgentID: core.Trim(agentID),
CircuitBreaker: s.brainCircuitBreaker(),
})
}
func (s *PrepSubsystem) brainCircuitBreaker() *brainclient.CircuitBreaker {
state := s.brainClientState()
if state == nil {
return nil
}
state.once.Do(func() {
state.breaker = brainclient.NewCircuitBreaker(brainclient.CircuitBreakerOptions{})
})
return state.breaker
}
func (s *PrepSubsystem) brainClientState() *brainClientState {
if s == nil {
return nil
}
s.brainStateOnce.Do(func() {
s.brainState = &brainClientState{}
})
return s.brainState
}
func (s *PrepSubsystem) scopedBrainBody(body map[string]any) map[string]any {
if body == nil {
return nil
}
if org := core.Trim(core.Env("CORE_BRAIN_ORG")); org != "" && !brainValuePresent(body["org"]) {
body["org"] = org
}
return body
}
func brainPayloadMap(payload map[string]any) map[string]any {
if data, ok := payload["data"].(map[string]any); ok && len(data) > 0 {
return data
}
return payload
}
func brainValuePresent(value any) bool {
if value == nil {
return false
}
return core.Trim(core.Sprint(value)) != ""
}