Phase 12 (ADR-0005). Capabilities are registered in code with parameter schemas, risk classes and default effects; sudo_shell and delete_database are locked to DENY and cannot be relaxed. Policies are scoped globally or per mind with capability, target glob, action kind and optional hour window; evaluation is deterministic, most specific wins, and ties go to the stricter effect. Invoke is the single entry point: it refuses dreaming and replay contexts, validates parameters (unknown keys, non http URLs and path traversal rejected), evaluates policy and session grants, hashes the exact request and records a policy_decision event. ALLOW executes with timeout and panic isolation and turns textual output into a tool-trust observation; ASK creates an approval request bound to the hash with an alert notification; DENY is counted and recorded. Operators approve once, approve for a time-boxed session grant, deny with a note, or modify parameters (re-validated, re-hashed, original kept in the append-only decision log). The self-model's capability list is recomputed from effective policy on start and on every policy change. The act process maps fetch requests to http.fetch and the action handler invokes through the layer; replies report capabilities and outcomes. Policy matrix and approval centre pages, dashboard count, API, CLI (approvals, policy, invoke) and docs/capabilities.md.
72 lines
2.1 KiB
Go
72 lines
2.1 KiB
Go
package cognition
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"log/slog"
|
|
|
|
"github.com/andydixon/noema/internal/capabilities"
|
|
evstore "github.com/andydixon/noema/internal/events"
|
|
"github.com/andydixon/noema/internal/minds"
|
|
"github.com/andydixon/noema/pkg/events"
|
|
"github.com/andydixon/noema/pkg/ids"
|
|
)
|
|
|
|
// SelfModelSync keeps each mind's self-model capability list equal to the
|
|
// effective policy, so the mind cannot claim tools it does not have.
|
|
type SelfModelSync struct {
|
|
Minds *minds.Store
|
|
Caps *capabilities.Service
|
|
Events *evstore.Store
|
|
Log *slog.Logger
|
|
}
|
|
|
|
// Sync updates one mind (or all when mind is Nil) and emits
|
|
// self_model_updated only when something changed.
|
|
func (s SelfModelSync) Sync(ctx context.Context, mind ids.ID) {
|
|
var list []minds.Mind
|
|
if mind.IsZero() {
|
|
list, _ = s.Minds.List(ctx, false)
|
|
} else if m, err := s.Minds.Get(ctx, mind); err == nil {
|
|
list = []minds.Mind{m}
|
|
}
|
|
for _, m := range list {
|
|
inv, err := s.Caps.Inventory(ctx, m.ID)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
sm, err := s.Minds.GetSelfModel(ctx, m.ID)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
var caps []minds.CapabilityEntry
|
|
for _, i := range inv {
|
|
eff := string(i.Effect)
|
|
if i.Capability.Locked {
|
|
eff = "unavailable"
|
|
}
|
|
caps = append(caps, minds.CapabilityEntry{Name: i.Capability.Name, Description: i.Capability.Description, Policy: eff})
|
|
}
|
|
if fmt.Sprint(caps) == fmt.Sprint(sm.Capabilities) {
|
|
continue
|
|
}
|
|
sm.Capabilities = caps
|
|
sm.Refresh(m.Personality)
|
|
if err := s.Minds.SaveSelfModel(ctx, m.ID, sm); err != nil {
|
|
s.Log.Warn("self-model save failed", "mind", m.Slug, "err", err)
|
|
continue
|
|
}
|
|
allow, ask := 0, 0
|
|
for _, c := range caps {
|
|
switch c.Policy {
|
|
case "allow":
|
|
allow++
|
|
case "ask":
|
|
ask++
|
|
}
|
|
}
|
|
s.Events.Append(ctx, events.New{MindID: m.ID, Type: events.SelfModelUpdated, Source: "capabilities", Summary: fmt.Sprintf("self-model: %d capabilities allowed, %d need approval, rest denied", allow, ask), //nolint:errcheck
|
|
Salience: 0.3, Confidence: 1, Metadata: map[string]any{"allow": allow, "ask": ask}, Payload: map[string]any{"capabilities": caps}})
|
|
}
|
|
}
|