// Package restrict provides the tool-level security policy engine prototype // (DevDocs/design/11 §3). It decides whether a tool/capability may be invoked in // a given actor environment by reading the capability's editable Boolean markers // AllowInGroup / AllowInStranger, which default to false so a capability is // owner-only until explicitly allowed. // // M5 delivers the marker data structure (the ToolPolicy on // internal/capability/registry.Capability) and the decision function. The // authoritative actor tagging happens in the channel/session layer in M6 — here // an ActorEnv is supplied directly, and the engine reads the registry's markers // to allow or deny. The engine is fail-closed: an unknown tool, an unknown // environment, or a nil registry all deny. package restrict import ( "io" "log/slog" "git.yeij.top/AskaEth/Cyrene_v2/internal/capability/registry" "git.yeij.top/AskaEth/Cyrene_v2/internal/logger" ) // ActorEnv is the environment in which a tool may be invoked, derived by the // channel/session layer from protocol-confirmed sender identity plus the owner's // whitelist — never from prompt inference. type ActorEnv string const ( // ActorOwner is the primary owner's private session. ActorOwner ActorEnv = "owner" // ActorGroupMember is a chat group where the owner is present. ActorGroupMember ActorEnv = "group_member" // ActorStranger is a direct message from an unrecognized sender. ActorStranger ActorEnv = "stranger" ) // Valid reports whether e is one of the known actor environments. func (e ActorEnv) Valid() bool { switch e { case ActorOwner, ActorGroupMember, ActorStranger: return true default: return false } } // Matcher is the subset of a capability registry the policy engine reads. It is // implemented by *registry.Registry. type Matcher interface { // Policy returns the editable reachability markers for the named capability. Policy(name string) (registry.ToolPolicy, error) } // Compile-time assertion that the registry satisfies Matcher, expressed here so // a breaking registry change fails at the restrict build. var _ Matcher = (*registry.Registry)(nil) // Policy is an actor-environment decision engine. It is immutable after it is // built, so it is safe for concurrent use. type Policy struct { reg Matcher env ActorEnv log *slog.Logger } // SetContext builds a Policy scoped to env, reading tool/capability markers from // reg. The runtime (M6) binds an actor environment to an engine through this // entry point. A nil reg yields a fail-closed policy that denies every tool. func SetContext(reg Matcher, env ActorEnv) *Policy { return &Policy{ reg: reg, env: env, log: slog.New(slog.NewTextHandler(io.Discard, nil)), } } // WithLogger returns a copy of p that logs denials through l with the // capability/restrict source tag. It does not mutate p, so it is safe to call // alongside concurrent Allow calls on p. func (p *Policy) WithLogger(l *slog.Logger) *Policy { if p == nil || l == nil { return p } cp := *p cp.log = logger.WithSource(l, "capability/restrict") return &cp } // Env returns the actor environment the policy was built for. func (p *Policy) Env() ActorEnv { if p == nil { return "" } return p.env } // Allow reports whether the tool/capability named name may be invoked in p's // environment. It reads the capability's editable markers. The owner environment // always allows; a nil registry, an unknown tool, an unknown environment, or a // false marker all deny (fail closed). func (p *Policy) Allow(name string) bool { if p == nil { return false } if p.env == ActorOwner { return true } if p.reg == nil { p.logDeny(name, "nil registry", "") return false } pol, err := p.reg.Policy(name) if err != nil { p.logDeny(name, "policy error", err.Error()) return false } switch p.env { case ActorGroupMember: if !pol.AllowInGroup { p.logDeny(name, "denied in group", "allow_in_group=false") return false } return true case ActorStranger: if !pol.AllowInStranger { p.logDeny(name, "denied to stranger", "allow_in_stranger=false") return false } return true default: p.logDeny(name, "unknown env", string(p.env)) return false } } // Deny is the inverse of Allow. func (p *Policy) Deny(name string) bool { return !p.Allow(name) } func (p *Policy) logDeny(name, reason, detail string) { if p == nil || p.log == nil { return } args := []any{"tool", name, "reason", reason} if detail != "" { args = append(args, "detail", detail) } p.log.Warn("tool invocation denied by policy", args...) }