149 lines
4.5 KiB
Go
149 lines
4.5 KiB
Go
// 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...)
|
|
}
|