proxy

package
v0.5.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 7, 2026 License: Apache-2.0 Imports: 20 Imported by: 0

Documentation

Index

Constants

View Source
const (
	SessionCookieName = "ag_session"
	CSRFCookieName    = "ag_csrf"
	CSRFHeaderName    = "X-CSRF-Token"
	SessionTTL        = 1 * time.Hour
	// MaxSessions caps the in-memory session table. New logins at capacity
	// are rejected with 503 (see ErrSessionStoreFull) rather than silently
	// evicting the oldest live session — silent eviction used to log out a
	// real operator mid-approval if a thundering herd of dashboard tabs
	// racked up sessions. Expired entries are swept before rejecting, so a
	// store full of stale sessions does not block new logins.
	MaxSessions = 1024
	// SessionStoreFullRetryAfterSeconds is the Retry-After value returned on
	// 503 when MaxSessions is exhausted. 5s is short enough for a human to
	// notice the delay and long enough to avoid hammering the endpoint.
	SessionStoreFullRetryAfterSeconds = 5
)

Session cookie and CSRF header names used by the dashboard.

Double-submit cookie pattern:

  • ag_session is HttpOnly: used by the server to validate the session.
  • ag_csrf is JS-readable: the dashboard reads it via document.cookie and echoes its value in X-CSRF-Token. Both cookies carry the SAME token, so the server compares the header against the session token. An attacker on another origin cannot read either cookie (same-origin policy), so cannot forge the X-CSRF-Token header.
View Source
const (
	// DefaultAuditQueryLimit is the entry count returned when the client does
	// not supply ?limit=.
	DefaultAuditQueryLimit = 100
	// MaxAuditQueryLimit is the hard ceiling on ?limit=. Clients asking for
	// more receive this many entries (silently clamped); the ceiling exists
	// so a client cannot request an unbounded scan of the audit file.
	MaxAuditQueryLimit = 1000
	// SSEChannelBufferSize is the buffer size for Server-Sent Events channels.
	SSEChannelBufferSize = 64
	// ApprovalIDPrefix is the prefix for generated approval IDs.
	ApprovalIDPrefix = "ap_"
	// ShutdownTimeout is the graceful shutdown deadline.
	ShutdownTimeout = 10 * time.Second
	// MaxRequestBodySize is the maximum allowed size of incoming request bodies (1 MB).
	MaxRequestBodySize = 1 << 20
	// MaxPendingApprovals is the maximum number of entries (pending + resolved) kept
	// in the approval queue. When at capacity, the oldest resolved entry is
	// evicted first (LRU); if every slot holds an unresolved entry, new
	// approvals are rejected with 503 rather than silently dropped.
	MaxPendingApprovals = 10000
	// ApprovalQueueFullRetryAfterSeconds is the Retry-After value returned
	// when the approval queue is full and no resolved entries are available
	// to evict. Operators tune MaxPendingApprovals or drain pending items;
	// clients should back off roughly this long before retrying.
	ApprovalQueueFullRetryAfterSeconds = 30
	// SchemaVersionV1 is the wire-protocol version emitted on every
	// /v1/check response and accepted on every /v1/check request. Clients
	// may omit the field on requests (defaults to v1); any other value is
	// rejected with HTTP 400. The full schema lives in
	// pkg/proxy/schema/v1/schema.json and is documented in
	// docs/WIRE_PROTOCOL.md.
	SchemaVersionV1 = "v1"
)

Variables

View Source
var ErrApprovalQueueFull = errors.New("approval queue full: no resolved entries to evict")

ErrApprovalQueueFull is returned by ApprovalQueue.Add when the queue is at capacity and every entry is still unresolved. The HTTP handler maps this to 503 + Retry-After so the caller knows to back off rather than treating it as a generic 500.

View Source
var ErrSessionStoreFull = errors.New("session store full: too many active sessions")

ErrSessionStoreFull is returned by SessionStore.Create when every slot is held by a live (non-expired) session. handleLogin maps this to 503 + Retry-After so the dashboard shows a visible failure instead of the old silent evict-the-oldest behavior.

Functions

func TenantIDFromContext added in v0.5.0

func TenantIDFromContext(ctx context.Context) string

TenantIDFromContext returns the tenant ID stamped on ctx by withTenant, or LocalTenantID when nothing is set (legacy /v1/... routes that did not run through withTenant). The empty string is also coerced to LocalTenantID so callers never have to special-case it.

func WithTenantID added in v0.5.0

func WithTenantID(ctx context.Context, tenantID string) context.Context

WithTenantID returns a context derived from ctx that carries tenantID. Empty values are stored verbatim; readers (TenantIDFromContext) decide how to default.

Types

type ApprovalQueue

type ApprovalQueue struct {
	// contains filtered or unexported fields
}

ApprovalQueue manages pending approval requests.

maxSize caps the total number of entries (resolved + unresolved) so a spike of approval-required traffic with no operator around cannot exhaust memory. It is exposed as a field rather than a package const so tests can shrink it without faking 10 000 entries; production code always uses MaxPendingApprovals via NewServer.

func (*ApprovalQueue) Add

Add registers a new pending approval. If the queue is at capacity the oldest resolved entry is evicted first (LRU on CreatedAt; resolution does not rewind it, so the eviction target is the entry that has been around the longest). If every slot is still unresolved, Add returns ErrApprovalQueueFull and the caller is expected to surface 503 + Retry-After — silently dropping the request would leave the agent waiting forever on an ID that does not exist.

func (*ApprovalQueue) Broadcast

func (q *ApprovalQueue) Broadcast(event AuditEvent)

Broadcast sends an event to all SSE subscribers (public, acquires lock).

func (*ApprovalQueue) List

func (q *ApprovalQueue) List() []*PendingAction

func (*ApprovalQueue) Lookup added in v0.5.0

func (q *ApprovalQueue) Lookup(id string) (*PendingAction, bool)

Lookup returns a snapshot of the pending entry for the given ID. The bool result is false when the ID is unknown (e.g. expired/evicted, typo, or wrong tenant). Read-only — uses RLock.

The returned *PendingAction is a defensive copy: callers cannot mutate queue state through it. The handleCheck approval-id round- trip uses this on the hot path of every retry, so the read-lock keeps it cheap relative to the existing /v1/check evaluation cost.

func (*ApprovalQueue) PendingCount added in v0.5.0

func (q *ApprovalQueue) PendingCount() int

PendingCount returns the number of unresolved pending actions without allocating. Intended for metrics/health endpoints that just need the count.

func (*ApprovalQueue) Resolve

func (q *ApprovalQueue) Resolve(id string, decision policy.Decision) error

func (*ApprovalQueue) Subscribe

func (q *ApprovalQueue) Subscribe() chan AuditEvent

func (*ApprovalQueue) Unsubscribe

func (q *ApprovalQueue) Unsubscribe(ch chan AuditEvent)

type AuditEvent

type AuditEvent struct {
	Type      string               `json:"type"` // "check", "approval", "resolved"
	Timestamp time.Time            `json:"timestamp"`
	Transport string               `json:"transport,omitempty"`
	Request   policy.ActionRequest `json:"request"`
	Result    policy.CheckResult   `json:"result"`
}

AuditEvent is sent over SSE to dashboard clients for any check result.

Transport identifies the integration path that produced the event ("sdk", "mcp_gateway", "llm_api_proxy"). Defaults to "sdk" on the wire when unset so dashboard JS does not need a fallback. Added in v0.5 (Phase 4B, A19); pre-v0.5 SSE consumers see the field as an extra (ignored) JSON key.

type Config

type Config struct {
	Port             int
	Engine           *policy.Engine
	Logger           audit.Logger
	DashboardEnabled bool
	Notifier         *notify.Dispatcher
	// APIKey protects the approve/deny endpoints. If empty, a warning is
	// logged and the endpoints are open (suitable for localhost-only deployments).
	APIKey string
	// AllowedOrigin is returned in Access-Control-Allow-Origin. Defaults to
	// localhost only. Set to a specific origin or leave empty for localhost.
	AllowedOrigin string
	// BaseURL is the externally-reachable URL of this server, used to
	// construct approval URLs. Defaults to http://localhost:<Port>.
	BaseURL string
	// Version is the application version string shown in /health.
	Version string
	// TLSTerminatedUpstream tells the server that session cookies should be
	// issued with Secure set regardless of whether the incoming request has
	// r.TLS populated. Set this when AgentGuard runs behind a reverse proxy
	// that terminates TLS and does not forward X-Forwarded-Proto. Default
	// false preserves v0.4.0 behavior (cookie Secure keyed to r.TLS only).
	TLSTerminatedUpstream bool

	// SessionCostTTL bounds how long an idle session_id entry lingers in the
	// cost accumulator map. A periodic goroutine evicts entries whose last
	// write was more than TTL ago. Zero disables the sweep (v0.4.0 behavior:
	// entries accumulate for the process lifetime).
	SessionCostTTL time.Duration
	// SessionCostSweepInterval controls how often the sweeper runs. If zero
	// and SessionCostTTL > 0, it defaults to SessionCostTTL/4 with a floor
	// of 1 minute.
	SessionCostSweepInterval time.Duration

	// SessionTTL overrides the dashboard session cookie lifetime. Zero or
	// negative falls back to SessionTTL (the package-level default). Wired
	// from policy's proxy.session.ttl.
	SessionTTL time.Duration
	// MaxRequestBodyBytes overrides the POST /v1/check body cap. Zero or
	// negative falls back to MaxRequestBodySize. Wired from policy's
	// proxy.request.max_body_bytes.
	MaxRequestBodyBytes int64
	// AuditDefaultLimit overrides the default ?limit= on /v1/audit. Zero or
	// negative falls back to DefaultAuditQueryLimit. Wired from policy's
	// proxy.audit.default_limit.
	AuditDefaultLimit int
	// AuditMaxLimit overrides the hard ceiling on ?limit= for /v1/audit.
	// Zero or negative falls back to MaxAuditQueryLimit. Wired from
	// policy's proxy.audit.max_limit.
	AuditMaxLimit int
}

Config holds the server configuration.

type PendingAction

type PendingAction struct {
	ID        string               `json:"id"`
	Request   policy.ActionRequest `json:"request"`
	Result    policy.CheckResult   `json:"result"`
	CreatedAt time.Time            `json:"created_at"`
	Resolved  bool                 `json:"resolved"`
	Decision  string               `json:"decision,omitempty"`
}

PendingAction is an action waiting for human approval.

type Server

type Server struct {
	// contains filtered or unexported fields
}

Server is the AgentGuard HTTP proxy.

func NewServer

func NewServer(cfg Config) *Server

NewServer creates a new proxy server.

func (*Server) Handler added in v0.5.0

func (s *Server) Handler() http.Handler

Handler returns the fully wired http.Handler (recoverPanic → withCORS → withTraffic → withLogging → mux). Exposed for embedders and integration tests that want to drive the server through httptest.NewServer rather than binding a real port. Stable contract — callers should NOT inspect or wrap individual middleware.

func (*Server) LastRequestAt added in v0.5.0

func (s *Server) LastRequestAt() time.Time

LastRequestAt returns the wall-clock time of the most recent HTTP request, or the zero time if no request has been observed yet.

func (*Server) Shutdown

func (s *Server) Shutdown()

Shutdown gracefully stops the server. Safe to call multiple times.

func (*Server) Start

func (s *Server) Start() error

Start begins listening for requests.

type Session added in v0.5.0

type Session struct {
	Token     string
	ExpiresAt time.Time
}

Session represents an authenticated dashboard session. The token value is stored in an HTTP-only cookie AND returned in the login response body so the browser JS can send it back as an X-CSRF-Token header on state-changing requests (double-submit cookie pattern).

type SessionStore added in v0.5.0

type SessionStore struct {
	// contains filtered or unexported fields
}

SessionStore holds active dashboard sessions in memory.

func NewSessionStore added in v0.5.0

func NewSessionStore() *SessionStore

NewSessionStore creates an empty in-memory session store using the package-default SessionTTL. Prefer NewSessionStoreWithTTL in wiring code that reads the TTL from policy config.

func NewSessionStoreWithTTL added in v0.5.0

func NewSessionStoreWithTTL(d time.Duration) *SessionStore

NewSessionStoreWithTTL creates an empty store whose Create() will issue sessions that expire after d. A non-positive d falls back to SessionTTL so callers can pass the raw policy value without an extra guard.

func (*SessionStore) Count added in v0.5.0

func (s *SessionStore) Count() int

Count returns the number of active sessions (for testing).

func (*SessionStore) Create added in v0.5.0

func (s *SessionStore) Create() (Session, error)

Create issues a new session token with the configured TTL.

When the store is already at MaxSessions, Create first sweeps expired entries (cheap dead-weight cleanup) and only returns ErrSessionStoreFull if every remaining slot is still live. This replaces v0.4.0's silent evict-the-oldest-live-session behavior — that silently kicked a real operator out mid-approval whenever a rogue set of dashboard tabs racked up more than MaxSessions entries.

func (*SessionStore) Destroy added in v0.5.0

func (s *SessionStore) Destroy(token string)

Destroy removes a session token.

func (*SessionStore) Validate added in v0.5.0

func (s *SessionStore) Validate(token string) bool

Validate returns true iff the provided token exists and is not expired. Expired tokens are lazily removed.

Directories

Path Synopsis
schema
v1
Package v1 documents and re-exports the v1 wire-protocol types for the AgentGuard /v1/check endpoint.
Package v1 documents and re-exports the v1 wire-protocol types for the AgentGuard /v1/check endpoint.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL