Implements the security wall between non-aligned agents (issue #97). Adds pkg/trust with: - Three trust tiers: Full (Tier 3), Verified (Tier 2), Untrusted (Tier 1) - Agent registry with mutex-protected concurrent access - Policy engine with capability-based access control - Repo-scoped permissions for Tier 2 agents - Default policies matching the spec (rate limits, approval gates, denials) - 49 tests covering all tiers, capabilities, edge cases, and helpers Closes #97 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
165 lines
4.3 KiB
Go
165 lines
4.3 KiB
Go
// Package trust implements an agent trust model with tiered access control.
|
|
//
|
|
// Agents are assigned trust tiers that determine their capabilities:
|
|
//
|
|
// - Tier 3 (Full Trust): Internal agents with full access (e.g., Athena, Virgil, Charon)
|
|
// - Tier 2 (Verified): Partner agents with scoped access (e.g., Clotho, Hypnos)
|
|
// - Tier 1 (Untrusted): External/community agents with minimal access
|
|
//
|
|
// The package provides a Registry for managing agent identities and a PolicyEngine
|
|
// for evaluating capability requests against trust policies.
|
|
package trust
|
|
|
|
import (
|
|
"fmt"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// Tier represents an agent's trust level in the system.
|
|
type Tier int
|
|
|
|
const (
|
|
// TierUntrusted is for external/community agents with minimal access.
|
|
TierUntrusted Tier = 1
|
|
// TierVerified is for partner agents with scoped access.
|
|
TierVerified Tier = 2
|
|
// TierFull is for internal agents with full access.
|
|
TierFull Tier = 3
|
|
)
|
|
|
|
// String returns the human-readable name of the tier.
|
|
func (t Tier) String() string {
|
|
switch t {
|
|
case TierUntrusted:
|
|
return "untrusted"
|
|
case TierVerified:
|
|
return "verified"
|
|
case TierFull:
|
|
return "full"
|
|
default:
|
|
return fmt.Sprintf("unknown(%d)", int(t))
|
|
}
|
|
}
|
|
|
|
// Valid returns true if the tier is a recognised trust level.
|
|
func (t Tier) Valid() bool {
|
|
return t >= TierUntrusted && t <= TierFull
|
|
}
|
|
|
|
// Capability represents a specific action an agent can perform.
|
|
type Capability string
|
|
|
|
const (
|
|
CapPushRepo Capability = "repo.push"
|
|
CapMergePR Capability = "pr.merge"
|
|
CapCreatePR Capability = "pr.create"
|
|
CapCreateIssue Capability = "issue.create"
|
|
CapCommentIssue Capability = "issue.comment"
|
|
CapReadSecrets Capability = "secrets.read"
|
|
CapRunPrivileged Capability = "cmd.privileged"
|
|
CapAccessWorkspace Capability = "workspace.access"
|
|
CapModifyFlows Capability = "flows.modify"
|
|
)
|
|
|
|
// Agent represents an agent identity in the trust system.
|
|
type Agent struct {
|
|
// Name is the unique identifier for the agent (e.g., "Athena", "Clotho").
|
|
Name string
|
|
// Tier is the agent's trust level.
|
|
Tier Tier
|
|
// ScopedRepos limits repo access for Tier 2 agents. Empty means no repo access.
|
|
// Tier 3 agents ignore this field (they have access to all repos).
|
|
ScopedRepos []string
|
|
// RateLimit is the maximum requests per minute. 0 means unlimited.
|
|
RateLimit int
|
|
// TokenExpiresAt is when the agent's token expires.
|
|
TokenExpiresAt time.Time
|
|
// CreatedAt is when the agent was registered.
|
|
CreatedAt time.Time
|
|
}
|
|
|
|
// Registry manages agent identities and their trust tiers.
|
|
type Registry struct {
|
|
mu sync.RWMutex
|
|
agents map[string]*Agent
|
|
}
|
|
|
|
// NewRegistry creates an empty agent registry.
|
|
func NewRegistry() *Registry {
|
|
return &Registry{
|
|
agents: make(map[string]*Agent),
|
|
}
|
|
}
|
|
|
|
// Register adds or updates an agent in the registry.
|
|
// Returns an error if the agent name is empty or the tier is invalid.
|
|
func (r *Registry) Register(agent Agent) error {
|
|
if agent.Name == "" {
|
|
return fmt.Errorf("trust.Register: agent name is required")
|
|
}
|
|
if !agent.Tier.Valid() {
|
|
return fmt.Errorf("trust.Register: invalid tier %d for agent %q", agent.Tier, agent.Name)
|
|
}
|
|
if agent.CreatedAt.IsZero() {
|
|
agent.CreatedAt = time.Now()
|
|
}
|
|
if agent.RateLimit == 0 {
|
|
agent.RateLimit = defaultRateLimit(agent.Tier)
|
|
}
|
|
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
r.agents[agent.Name] = &agent
|
|
return nil
|
|
}
|
|
|
|
// Get returns the agent with the given name, or nil if not found.
|
|
func (r *Registry) Get(name string) *Agent {
|
|
r.mu.RLock()
|
|
defer r.mu.RUnlock()
|
|
return r.agents[name]
|
|
}
|
|
|
|
// Remove deletes an agent from the registry.
|
|
func (r *Registry) Remove(name string) bool {
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
if _, ok := r.agents[name]; !ok {
|
|
return false
|
|
}
|
|
delete(r.agents, name)
|
|
return true
|
|
}
|
|
|
|
// List returns all registered agents. The returned slice is a snapshot.
|
|
func (r *Registry) List() []Agent {
|
|
r.mu.RLock()
|
|
defer r.mu.RUnlock()
|
|
out := make([]Agent, 0, len(r.agents))
|
|
for _, a := range r.agents {
|
|
out = append(out, *a)
|
|
}
|
|
return out
|
|
}
|
|
|
|
// Len returns the number of registered agents.
|
|
func (r *Registry) Len() int {
|
|
r.mu.RLock()
|
|
defer r.mu.RUnlock()
|
|
return len(r.agents)
|
|
}
|
|
|
|
// defaultRateLimit returns the default rate limit for a given tier.
|
|
func defaultRateLimit(t Tier) int {
|
|
switch t {
|
|
case TierUntrusted:
|
|
return 10
|
|
case TierVerified:
|
|
return 60
|
|
case TierFull:
|
|
return 0 // unlimited
|
|
default:
|
|
return 10
|
|
}
|
|
}
|