Documentation
¶
Overview ¶
Package guard provides security context management and guard registry for the MCP Gateway.
This package is responsible for managing security labels (DIFC - Decentralized Information Flow Control) and storing/retrieving agent identifiers in request contexts.
Relationship with internal/auth:
- internal/auth: Primary authentication logic (header parsing, validation) - internal/guard: Security context management (agent ID tracking, guard registry)
For authentication-related operations, always use the internal/auth package directly.
Example:
// Extract agent ID from auth header and store in context agentID := auth.ExtractAgentID(authHeader) ctx = guard.SetAgentIDInContext(ctx, agentID) // Retrieve agent ID from context agentID := guard.GetAgentIDFromContext(ctx) // Returns "default" if not found
Index ¶
- Variables
- func ApplyLabelAgentResult(result *LabelAgentResult, agentLabels *difc.AgentLabels, ...) (difc.EnforcementMode, error)
- func BuildLabelAgentPayload(policy interface{}, trustedBots []string, trustedUsers []string) interface{}
- func CloseGlobalCompilationCache(ctx context.Context) error
- func ConfigureGlobalCompilationCache(ctx context.Context, dir string) error
- func FindServerWASMGuardFile(serverID string) (string, bool, error)
- func GetAgentIDFromContext(ctx context.Context) string
- func GetRegisteredGuardTypes() []string
- func GetWASMGuardsRootDir() string
- func RegisterGuardType(name string, factory GuardFactory)
- func RunPipelinePhase4(ctx context.Context, in PipelineInput, pre *PipelinePreResult, ...) (difc.LabeledData, error)
- func RunPipelinePhase6(pre *PipelinePreResult, labeledData difc.LabeledData, ...)
- func SetAgentIDInContext(ctx context.Context, agentID string) context.Context
- func SetRequestStateInContext(ctx context.Context, state RequestState) context.Context
- type AgentLabelsPayload
- type BackendCaller
- type ContextKey
- type Guard
- type GuardFactory
- type LabelAgentResult
- type NoopGuard
- func (g *NoopGuard) LabelAgent(ctx context.Context, policy interface{}, backend BackendCaller, ...) (*LabelAgentResult, error)
- func (g *NoopGuard) LabelResource(ctx context.Context, toolName string, args interface{}, backend BackendCaller, ...) (*difc.LabeledResource, difc.OperationType, error)
- func (g *NoopGuard) LabelResponse(ctx context.Context, toolName string, result interface{}, ...) (difc.LabeledData, error)
- func (g *NoopGuard) Name() string
- type PipelineAccessDenied
- type PipelineInput
- type PipelinePreResult
- type Registry
- func (r *Registry) Close(ctx context.Context)
- func (r *Registry) Get(serverID string) Guard
- func (r *Registry) GetGuardInfo() map[string]string
- func (r *Registry) Has(serverID string) bool
- func (r *Registry) HasNonNoopGuard() bool
- func (r *Registry) List() []string
- func (r *Registry) Register(serverID string, guard Guard)
- func (r *Registry) Remove(serverID string)
- type RequestState
- type WasmGuard
- func NewWasmGuard(ctx context.Context, name string, wasmPath string, backend BackendCaller) (*WasmGuard, error)
- func NewWasmGuardFromBytes(ctx context.Context, name string, wasmBytes []byte, backend BackendCaller) (*WasmGuard, error)
- func NewWasmGuardWithOptions(ctx context.Context, name string, wasmBytes []byte, backend BackendCaller, ...) (*WasmGuard, error)
- func (g *WasmGuard) Close(ctx context.Context) error
- func (g *WasmGuard) IsHealthy() bool
- func (g *WasmGuard) LabelAgent(ctx context.Context, policy any, backend BackendCaller, ...) (*LabelAgentResult, error)
- func (g *WasmGuard) LabelResource(ctx context.Context, toolName string, args any, backend BackendCaller, ...) (*difc.LabeledResource, difc.OperationType, error)
- func (g *WasmGuard) LabelResponse(ctx context.Context, toolName string, result any, backend BackendCaller, ...) (difc.LabeledData, error)
- func (g *WasmGuard) Name() string
- type WasmGuardOptions
- type WriteSinkGuard
- func (g *WriteSinkGuard) LabelAgent(_ context.Context, _ interface{}, _ BackendCaller, _ *difc.Capabilities) (*LabelAgentResult, error)
- func (g *WriteSinkGuard) LabelResource(_ context.Context, toolName string, _ interface{}, _ BackendCaller, ...) (*difc.LabeledResource, difc.OperationType, error)
- func (g *WriteSinkGuard) LabelResponse(_ context.Context, _ string, _ interface{}, _ BackendCaller, ...) (difc.LabeledData, error)
- func (g *WriteSinkGuard) Name() string
Constants ¶
This section is empty.
Variables ¶
var AllowedIntegrityLevels = config.AllIntegrityLevels()
AllowedIntegrityLevels is derived from the canonical integrity levels in config.
Functions ¶
func ApplyLabelAgentResult ¶ added in v0.3.1
func ApplyLabelAgentResult(result *LabelAgentResult, agentLabels *difc.AgentLabels, defaultMode difc.EnforcementMode) (difc.EnforcementMode, error)
ApplyLabelAgentResult applies the agent labels from a LabelAgentResult to the given AgentLabels using batch helpers (minimizing mutex acquisitions), and returns the effective enforcement mode. If result.DIFCMode is empty, defaultMode is returned unchanged. If result.DIFCMode is non-empty but cannot be parsed, an error is returned.
func BuildLabelAgentPayload ¶ added in v0.1.20
func BuildLabelAgentPayload(policy interface{}, trustedBots []string, trustedUsers []string) interface{}
BuildLabelAgentPayload constructs the label_agent input payload from the given guard policy and optional lists of additional trusted bot usernames and trusted user logins. The trusted bots are merged with the guard's built-in list and cannot remove any built-in entries. If both trustedBots and trustedUsers are nil or empty, the returned payload contains only the allow-only policy.
func CloseGlobalCompilationCache ¶ added in v0.3.1
CloseGlobalCompilationCache releases JIT resources held by the shared compilation cache. It must be called once during graceful shutdown, after all WasmGuard runtimes have been closed (i.e., after Registry.Close()). Calling it while guards are still active or calling it more than once leads to undefined behavior. It is not safe to call concurrently.
func ConfigureGlobalCompilationCache ¶ added in v0.3.7
ConfigureGlobalCompilationCache replaces the process-level compilation cache. This should be called during process startup before any guards are created.
func FindServerWASMGuardFile ¶ added in v0.3.25
FindServerWASMGuardFile discovers the first .wasm file for a server under $MCP_GATEWAY_WASM_GUARDS_DIR/<serverID>.
func GetAgentIDFromContext ¶
GetAgentIDFromContext extracts the agent ID from the context Returns "default" if not found
func GetRegisteredGuardTypes ¶
func GetRegisteredGuardTypes() []string
GetRegisteredGuardTypes returns all registered guard type names
func GetWASMGuardsRootDir ¶ added in v0.3.25
func GetWASMGuardsRootDir() string
GetWASMGuardsRootDir returns the trimmed value of MCP_GATEWAY_WASM_GUARDS_DIR.
func RegisterGuardType ¶
func RegisterGuardType(name string, factory GuardFactory)
RegisterGuardType registers a guard type with a factory function This allows dynamic guard creation by name
func RunPipelinePhase4 ¶ added in v0.3.21
func RunPipelinePhase4(ctx context.Context, in PipelineInput, pre *PipelinePreResult, responseData interface{}) (difc.LabeledData, error)
RunPipelinePhase4 executes Phase 4 of the DIFC pipeline: guard labels the response.
When ShouldCallLabelResponse returns false for the given operation and enforcement mode, the call is skipped and (nil, nil) is returned. Callers should treat nil labeled data as "no fine-grained labels available; fall back to coarse-grained result".
The ctx must carry the request state set by RunPipelinePrePhases (tool args for the guard).
func RunPipelinePhase6 ¶ added in v0.3.21
func RunPipelinePhase6(pre *PipelinePreResult, labeledData difc.LabeledData, enforcementMode difc.EnforcementMode)
RunPipelinePhase6 executes Phase 6 of the DIFC pipeline: label accumulation.
When ShouldAccumulateReadLabels returns true for the operation and enforcement mode, the agent's label set is updated:
- If labeledData is non-nil, labels are accumulated from the response (labeledData.Overall()).
- If labeledData is nil, labels are accumulated from the resource determined in Phase 1. This ensures propagate-mode semantics are preserved even when fine-grained labeling was skipped or unavailable.
func SetAgentIDInContext ¶
SetAgentIDInContext sets the agent ID in the context
func SetRequestStateInContext ¶
func SetRequestStateInContext(ctx context.Context, state RequestState) context.Context
SetRequestStateInContext stores guard request state in context
Types ¶
type AgentLabelsPayload ¶ added in v0.1.10
type AgentLabelsPayload struct {
Secrecy []string `json:"secrecy"`
Integrity []string `json:"integrity"`
}
AgentLabelsPayload holds effective secrecy/integrity labels for the session.
type BackendCaller ¶
type BackendCaller interface {
// CallTool makes a read-only call to the backend MCP server
// This is used by guards to gather metadata for labeling
CallTool(ctx context.Context, toolName string, args interface{}) (interface{}, error)
}
BackendCaller provides a way for guards to make read-only calls to the backend to gather information needed for labeling (e.g., fetching issue author)
type ContextKey ¶
type ContextKey string
ContextKey is used for storing values in context
const ( // AgentIDContextKey stores the agent ID in the request context AgentIDContextKey ContextKey = "difc-agent-id" // RequestStateContextKey stores guard-specific request state RequestStateContextKey ContextKey = "difc-request-state" )
type Guard ¶
type Guard interface {
// Name returns the identifier for this guard (e.g., "github", "noop")
Name() string
// LabelAgent initializes guard policy and returns effective agent/session state
// for the current session.
// Returns:
// - result: effective labels, mode, and normalized policy
// - error: any validation/initialization error
LabelAgent(ctx context.Context, policy interface{}, backend BackendCaller, caps *difc.Capabilities) (*LabelAgentResult, error)
// LabelResource determines the resource being accessed and its labels
// This may call the backend (via BackendCaller) to gather metadata needed for labeling
// Returns:
// - resource: The labeled resource (simple or nested structure for fine-grained filtering)
// - operation: The type of operation (Read, Write, or ReadWrite)
// - error: Any error that occurred during labeling
LabelResource(ctx context.Context, toolName string, args interface{}, backend BackendCaller, caps *difc.Capabilities) (*difc.LabeledResource, difc.OperationType, error)
// LabelResponse labels the response data after a successful backend call
// This is used for fine-grained filtering of collections
// Returns:
// - labeledData: The response data with per-item labels (if applicable)
// - error: Any error that occurred during labeling
// If the guard returns nil for labeledData, the reference monitor will use the
// resource labels from LabelResource for the entire response
LabelResponse(ctx context.Context, toolName string, result interface{}, backend BackendCaller, caps *difc.Capabilities) (difc.LabeledData, error)
}
Guard handles DIFC labeling for a specific MCP server Guards ONLY label resources - they do NOT make access control decisions The Reference Monitor (in the server) uses guard-provided labels to enforce DIFC policies
func CreateGuard ¶
CreateGuard creates a guard instance by name using registered factories
type GuardFactory ¶
GuardFactory is a function that creates a guard instance
type LabelAgentResult ¶ added in v0.1.10
type LabelAgentResult struct {
Agent AgentLabelsPayload `json:"agent"`
DIFCMode string `json:"difc_mode"`
NormalizedPolicy map[string]interface{} `json:"normalized_policy,omitempty"`
}
LabelAgentResult describes the effective policy/session state returned by a guard.
func RunLabelAgent ¶ added in v0.3.2
func RunLabelAgent( ctx context.Context, g Guard, payload interface{}, backend BackendCaller, caps *difc.Capabilities, agentLabels *difc.AgentLabels, defaultMode difc.EnforcementMode, ) (difc.EnforcementMode, *LabelAgentResult, error)
RunLabelAgent executes the standard LabelAgent initialization pipeline:
- Calls the guard's LabelAgent method with the provided pre-built payload.
- Validates the result is non-nil.
- Applies the returned agent labels to agentLabels.
- Parses and returns the effective enforcement mode.
It returns the effective enforcement mode, the raw LabelAgentResult (so callers can inspect NormalizedPolicy, DIFCMode, etc.), and any error. On error, defaultMode is returned unchanged so the caller's mode is unaffected.
type NoopGuard ¶
type NoopGuard struct{}
NoopGuard is the default guard that performs no DIFC labeling It allows all operations by returning empty labels (no restrictions)
func (*NoopGuard) LabelAgent ¶ added in v0.1.10
func (g *NoopGuard) LabelAgent(ctx context.Context, policy interface{}, backend BackendCaller, caps *difc.Capabilities) (*LabelAgentResult, error)
LabelAgent initializes noop guard session state.
func (*NoopGuard) LabelResource ¶
func (g *NoopGuard) LabelResource(ctx context.Context, toolName string, args interface{}, backend BackendCaller, caps *difc.Capabilities) (*difc.LabeledResource, difc.OperationType, error)
LabelResource returns an empty resource with no label requirements Treats all operations as read-write (most conservative assumption)
func (*NoopGuard) LabelResponse ¶
func (g *NoopGuard) LabelResponse(ctx context.Context, toolName string, result interface{}, backend BackendCaller, caps *difc.Capabilities) (difc.LabeledData, error)
LabelResponse returns nil, indicating no fine-grained labeling The reference monitor will use the resource labels for the entire response
type PipelineAccessDenied ¶ added in v0.3.21
type PipelineAccessDenied struct {
EvalResult *difc.EvaluationResult
Resource *difc.LabeledResource
AgentLabels *difc.AgentLabels
}
PipelineAccessDenied is returned by RunPipelinePrePhases when Phase 2 denies access. Callers inspect EvalResult.Reason to formulate the appropriate denial response (HTTP 403 for the proxy, MCP error for the unified server).
func (*PipelineAccessDenied) Error ¶ added in v0.3.21
func (e *PipelineAccessDenied) Error() string
type PipelineInput ¶ added in v0.3.21
type PipelineInput struct {
// AgentID identifies the agent in the registry (e.g. "proxy" or an MCP session agent ID).
AgentID string
// ToolName is the guard tool name for this request.
ToolName string
// Args are the tool arguments; they are stored in context for LabelResponse (Phase 4).
Args interface{}
// Guard performs DIFC labeling operations (LabelResource, LabelResponse).
Guard Guard
// Evaluator runs coarse-grained and fine-grained DIFC access checks.
Evaluator *difc.Evaluator
// AgentRegistry holds per-agent label state.
AgentRegistry *difc.AgentRegistry
// Capabilities is the DIFC capability set passed to the guard.
Capabilities *difc.Capabilities
// EnforcementMode is the active DIFC enforcement mode.
EnforcementMode difc.EnforcementMode
// BackendCaller is used by the guard for metadata enrichment calls.
BackendCaller BackendCaller
}
PipelineInput holds the shared inputs used across DIFC pipeline phases 0–2, 4, and 6. Both the HTTP proxy and the MCP unified server populate this struct and pass it to the shared phase helpers, providing their context-specific Phase 3 and Phase 5 logic as inline code in their respective callers.
type PipelinePreResult ¶ added in v0.3.21
type PipelinePreResult struct {
// AgentLabels is the agent's current label set (updated in Phase 6 for propagate mode).
AgentLabels *difc.AgentLabels
// Resource is the labeled resource returned by Phase 1.
Resource *difc.LabeledResource
// Operation is the operation type determined in Phase 1.
Operation difc.OperationType
// CoarseOutcome is the Phase 2 coarse-grained check outcome.
CoarseOutcome difc.CoarseCheckOutcome
// EvalResult is the Phase 2 evaluation result (contains Reason for denial messages).
EvalResult *difc.EvaluationResult
}
PipelinePreResult holds the outputs from phases 0–2 of the DIFC pipeline. Callers use these fields when handling denial responses, Phase 5 filtering, and Phase 6 label accumulation.
func RunPipelinePrePhases ¶ added in v0.3.21
func RunPipelinePrePhases(ctx context.Context, in PipelineInput) (context.Context, *PipelinePreResult, error)
RunPipelinePrePhases executes phases 0–2 of the DIFC enforcement pipeline:
- Phase 0: Get or create agent labels from the registry and store tool args in context.
- Phase 1: Guard labels the resource (may call backend for metadata enrichment).
- Phase 2: Coarse-grained access check; returns *PipelineAccessDenied if access is blocked.
The returned context carries the request state (tool args) required by LabelResponse in Phase 4. Callers must use this context for Phase 3 and all subsequent phases.
type Registry ¶
type Registry struct {
// contains filtered or unexported fields
}
Registry manages guard instances for different MCP servers
func (*Registry) Close ¶ added in v0.2.20
Close closes all registered guards that implement Close(context.Context) error. It should be called during server shutdown to release WASM runtime resources.
func (*Registry) GetGuardInfo ¶
GetGuardInfo returns information about all registered guards
func (*Registry) HasNonNoopGuard ¶ added in v0.1.12
HasNonNoopGuard returns true if any registered guard is not a noop guard
type RequestState ¶
type RequestState interface{}
RequestState represents any state that the guard needs to pass from request to response This is useful when the guard needs to carry information from LabelResource to LabelResponse
func GetRequestStateFromContext ¶
func GetRequestStateFromContext(ctx context.Context) RequestState
GetRequestStateFromContext retrieves guard request state from context
type WasmGuard ¶ added in v0.1.10
type WasmGuard struct {
// contains filtered or unexported fields
}
WasmGuard implements Guard interface by executing a WASM module in-process The WASM module runs sandboxed within the gateway using wazero runtime Guards cannot make direct network calls - they receive a BackendCaller interface via host functions
Thread Safety: WASM modules are single-threaded, so all calls to a guard instance are serialized using a mutex. Concurrent requests will queue and execute one at a time.
func NewWasmGuard ¶ added in v0.1.10
func NewWasmGuard(ctx context.Context, name string, wasmPath string, backend BackendCaller) (*WasmGuard, error)
NewWasmGuard creates a new WASM guard from a WASM binary file
func NewWasmGuardFromBytes ¶ added in v0.1.10
func NewWasmGuardFromBytes(ctx context.Context, name string, wasmBytes []byte, backend BackendCaller) (*WasmGuard, error)
NewWasmGuardFromBytes creates a new WASM guard from WASM binary bytes This is useful when loading guards from URLs or other sources
func NewWasmGuardWithOptions ¶ added in v0.1.10
func NewWasmGuardWithOptions(ctx context.Context, name string, wasmBytes []byte, backend BackendCaller, opts *WasmGuardOptions) (*WasmGuard, error)
NewWasmGuardWithOptions creates a new WASM guard from WASM binary bytes with custom options Options can be nil to use defaults (stdout/stderr go to os.Stderr/os.Stderr)
func (*WasmGuard) IsHealthy ¶ added in v0.3.26
IsHealthy reports whether the guard is still usable after previous WASM calls.
func (*WasmGuard) LabelAgent ¶ added in v0.1.10
func (g *WasmGuard) LabelAgent(ctx context.Context, policy any, backend BackendCaller, caps *difc.Capabilities) (*LabelAgentResult, error)
LabelAgent calls the WASM module's label_agent function.
func (*WasmGuard) LabelResource ¶ added in v0.1.10
func (g *WasmGuard) LabelResource(ctx context.Context, toolName string, args any, backend BackendCaller, caps *difc.Capabilities) (*difc.LabeledResource, difc.OperationType, error)
LabelResource calls the WASM module's label_resource function
func (*WasmGuard) LabelResponse ¶ added in v0.1.10
func (g *WasmGuard) LabelResponse(ctx context.Context, toolName string, result any, backend BackendCaller, caps *difc.Capabilities) (difc.LabeledData, error)
LabelResponse calls the WASM module's label_response function
type WasmGuardOptions ¶ added in v0.1.10
type WasmGuardOptions struct {
// Stdout is the writer for WASM stdout output. Defaults to os.Stderr if nil.
Stdout io.Writer
// Stderr is the writer for WASM stderr output. Defaults to os.Stderr if nil.
Stderr io.Writer
// CompilationCache overrides the process-level compilation cache.
// If nil and DisableCompilationCache is false, the shared globalCompilationCache is used.
CompilationCache wazero.CompilationCache
// DisableCompilationCache, when true, prevents any compilation cache from being used
// even if a global or per-instance cache is available. This can be useful to avoid
// unbounded memory growth when many distinct WASM binaries are loaded over the
// lifetime of a long-lived process.
DisableCompilationCache bool
}
WasmGuardOptions configures optional settings for WASM guard creation
type WriteSinkGuard ¶ added in v0.1.14
type WriteSinkGuard struct {
// contains filtered or unexported fields
}
WriteSinkGuard is a guard for write-only output channels (e.g., safe-outputs).
When DIFC is enabled, an agent that reads from a guarded server (like GitHub) acquires secrecy and integrity tags. Writing to an unguarded output server would fail because the DIFC evaluator sees a label mismatch: the agent has tags that the resource (with empty labels from a noop guard) does not.
The write-sink guard fixes this by:
- Returning empty labels from LabelAgent (does not contribute agent labels)
- Setting resource secrecy to the configured accept patterns
- Classifying all operations as OperationWrite
This ensures writes succeed because:
- Integrity: resource requires no tags (empty), agent has all zero required → OK
- Secrecy: resource secrecy includes the agent's secrecy patterns → agentSecrecy ⊆ resourceSecrecy → OK
Write-sink is required for ALL output servers when DIFC guards are enabled, including when repos="all" or repos="public". Without it, the noop guard assigns OperationRead + empty labels, causing integrity violations when the agent has integrity tags from other guards.
Configuration examples:
// For repos="all" or repos="public" (agent has no secrecy):
"guard-policies": {
"write-sink": {
"accept": ["*"]
}
}
// For scoped repos (agent has secrecy tags):
"guard-policies": {
"write-sink": {
"accept": ["private:github/gh-aw*"]
}
}
func NewWriteSinkGuard ¶ added in v0.1.14
func NewWriteSinkGuard(accept []string) *WriteSinkGuard
NewWriteSinkGuard creates a new write-sink guard with the specified accept patterns. Each pattern becomes a secrecy tag on the resource, allowing agents with matching secrecy to write to this sink.
func (*WriteSinkGuard) LabelAgent ¶ added in v0.1.14
func (g *WriteSinkGuard) LabelAgent(_ context.Context, _ interface{}, _ BackendCaller, _ *difc.Capabilities) (*LabelAgentResult, error)
LabelAgent returns empty labels. The write-sink does not contribute agent labels — those are set by the primary guard (e.g., the GitHub WASM guard).
func (*WriteSinkGuard) LabelResource ¶ added in v0.1.14
func (g *WriteSinkGuard) LabelResource(_ context.Context, toolName string, _ interface{}, _ BackendCaller, _ *difc.Capabilities) (*difc.LabeledResource, difc.OperationType, error)
LabelResource sets the resource's secrecy to the configured accept patterns and classifies the operation as a write.
For writes the DIFC evaluator checks:
- agentSecrecy ⊆ resource.Secrecy (no secret information leak)
- resource.Integrity ⊆ agentIntegrity (agent is trusted enough)
By setting the resource secrecy to the accept patterns from config, agents whose secrecy tags are a subset of the accept set can write successfully. By leaving the resource integrity empty, the second check also passes because the agent has all zero of the (empty) required integrity tags.
func (*WriteSinkGuard) LabelResponse ¶ added in v0.1.14
func (g *WriteSinkGuard) LabelResponse(_ context.Context, _ string, _ interface{}, _ BackendCaller, _ *difc.Capabilities) (difc.LabeledData, error)
LabelResponse returns nil; the write-sink does not perform fine-grained response labeling since all operations are writes (responses are confirmations).
func (*WriteSinkGuard) Name ¶ added in v0.1.14
func (g *WriteSinkGuard) Name() string
Name returns the identifier for this guard