Documentation
¶
Overview ¶
Package extensions defines interfaces for enterprise functionality.
This package provides extension points that allow AleutianEnterprise to add capabilities without modifying the core AleutianLocal codebase. The open source version uses no-op defaults for all interfaces.
Design Philosophy ¶
AleutianLocal is designed as a fully functional local utility that works offline without any external dependencies. Enterprise features are implemented by providing concrete implementations of these interfaces and injecting them via ServiceOptions.
Extension Categories ¶
The package is organized by domain:
- auth.go: Authentication and authorization (AuthProvider, AuthzProvider)
- audit.go: Compliance audit logging (AuditLogger)
- filter.go: Message transformation and PII redaction (MessageFilter)
- classifier.go: Data sensitivity classification (DataClassifier)
- request_auditor.go: Tamper-evident hash chain logging (RequestAuditor)
Usage in AleutianLocal (Open Source) ¶
The open source version uses no-op implementations:
opts := extensions.DefaultOptions() service := NewChatService(config, opts)
Usage in AleutianEnterprise ¶
Enterprise provides concrete implementations:
opts := extensions.ServiceOptions{
AuthProvider: enterprise.NewOktaProvider(config),
AuditLogger: enterprise.NewSplunkAuditor(config),
MessageFilter: enterprise.NewPIIFilter(policy),
}
service := NewChatService(config, opts)
Thread Safety ¶
All interface implementations must be safe for concurrent use. Multiple goroutines may call methods simultaneously.
See docs/code_quality_lessons/004_open_core_extension_patterns.md for detailed patterns and examples.
Index ¶
- Variables
- type AuditEvent
- type AuditFilter
- type AuditLogger
- type AuditableRequest
- type AuditableResponse
- type AuthInfo
- type AuthProvider
- type AuthzProvider
- type AuthzRequest
- type ChainVerificationResult
- type ClassificationFinding
- type ClassificationResult
- type DataClassification
- type DataClassifier
- type Detection
- type FilterResult
- type HTTPHeaders
- type HashChainEntry
- type MessageFilter
- type Metadata
- func (m Metadata) Clone() Metadata
- func (m Metadata) Delete(key string) Metadata
- func (m Metadata) Get(key string) (any, bool)
- func (m Metadata) GetBool(key string) (bool, bool)
- func (m Metadata) GetFloat64(key string) (float64, bool)
- func (m Metadata) GetInt(key string) (int, bool)
- func (m Metadata) GetInt64(key string) (int64, bool)
- func (m Metadata) GetString(key string) (string, bool)
- func (m Metadata) GetTime(key string) (time.Time, bool)
- func (m Metadata) Has(key string) bool
- func (m Metadata) Keys() []string
- func (m Metadata) Len() int
- func (m Metadata) Merge(other Metadata) Metadata
- func (m Metadata) Set(key string, value any) Metadata
- type NopAuditLogger
- type NopAuthProvider
- type NopAuthzProvider
- type NopDataClassifier
- type NopMessageFilter
- func (f *NopMessageFilter) FilterContext(ctx context.Context, contextMsg string) (*FilterResult, error)
- func (f *NopMessageFilter) FilterInput(ctx context.Context, message string) (*FilterResult, error)
- func (f *NopMessageFilter) FilterOutput(ctx context.Context, message string) (*FilterResult, error)
- type NopRequestAuditor
- func (a *NopRequestAuditor) CaptureRequest(_ context.Context, _ *AuditableRequest) (string, error)
- func (a *NopRequestAuditor) CaptureResponse(_ context.Context, _ string, _ *AuditableResponse) error
- func (a *NopRequestAuditor) GetChainLength(_ context.Context, _ string) (int, error)
- func (a *NopRequestAuditor) GetLastEntry(_ context.Context, _ string) (*HashChainEntry, error)
- func (a *NopRequestAuditor) RecordEntry(_ context.Context, _ HashChainEntry) error
- func (a *NopRequestAuditor) VerifyChain(_ context.Context, _ string) (*ChainVerificationResult, error)
- type RequestAuditor
- type ServiceOptions
- func (opts ServiceOptions) WithAudit(logger AuditLogger) ServiceOptions
- func (opts ServiceOptions) WithAuth(provider AuthProvider) ServiceOptions
- func (opts ServiceOptions) WithAuthz(provider AuthzProvider) ServiceOptions
- func (opts ServiceOptions) WithClassifier(classifier DataClassifier) ServiceOptions
- func (opts ServiceOptions) WithFilter(filter MessageFilter) ServiceOptions
- func (opts ServiceOptions) WithRequestAuditor(auditor RequestAuditor) ServiceOptions
Constants ¶
This section is empty.
Variables ¶
var ErrMessageBlocked = errors.New("message blocked by filter")
ErrMessageBlocked is returned when a message is rejected by the filter. Enterprise implementations should wrap this error with the reason.
Example:
if containsPII(msg) {
return "", fmt.Errorf("message contains PII: %w", ErrMessageBlocked)
}
ErrUnauthorized is returned when authentication or authorization fails. Enterprise implementations should wrap this error with additional context.
Example:
if !validToken {
return nil, fmt.Errorf("invalid token format: %w", extensions.ErrUnauthorized)
}
Functions ¶
This section is empty.
Types ¶
type AuditEvent ¶
type AuditEvent struct {
// EventType categorizes the event for filtering and alerting.
// Format: "category.action" (e.g., "auth.login", "chat.message")
EventType string
// Timestamp is when the event occurred (always use UTC).
// If zero, implementations should set to time.Now().UTC().
Timestamp time.Time
// UserID identifies who performed the action.
// Use "system" for automated actions, "anonymous" if unknown.
UserID string
// Action describes what operation was attempted.
// Common values: "create", "read", "update", "delete", "send", "receive"
Action string
// ResourceType is the category of resource involved.
// Examples: "message", "session", "evaluation", "model"
ResourceType string
// ResourceID is the specific resource instance (optional).
// Examples: "msg-123", "sess-456", "eval-789"
ResourceID string
// Outcome indicates the result of the action.
// Values: "success", "failure", "blocked", "error"
Outcome string
// Metadata holds additional event-specific data.
// This is where implementation-specific details go.
//
// Common metadata keys:
// - "error": error message if Outcome is "failure" or "error"
// - "ip_address": client IP for security analysis
// - "user_agent": client identifier
// - "duration_ms": operation duration for performance analysis
// - "model": AI model used
// - "session_id": conversation session
//
// Use NewMetadata() and type-safe accessors:
//
// Metadata: NewMetadata().
// Set("session_id", sessionID).
// Set("model", "claude-3"),
Metadata Metadata
}
AuditEvent represents a security-relevant event for compliance logging.
This struct captures the essential information needed for security audits, compliance reporting (GDPR, HIPAA, SOC2), and incident investigation.
Event Categories ¶
Events are categorized by type for filtering and alerting:
- Authentication: "auth.login", "auth.logout", "auth.failed"
- Authorization: "authz.denied", "authz.granted"
- Data Access: "data.read", "data.write", "data.delete"
- System: "system.start", "system.stop", "system.error"
- Chat: "chat.message", "chat.response", "chat.blocked"
Compliance Fields ¶
For regulatory compliance, always populate:
- UserID: Required for GDPR right-to-know requests
- Timestamp: Required for audit trail integrity
- ResourceType/ResourceID: Required for data lineage
Example:
event := AuditEvent{
EventType: "chat.message",
Timestamp: time.Now().UTC(),
UserID: authInfo.UserID,
Action: "send",
ResourceType: "message",
ResourceID: messageID,
Outcome: "success",
Metadata: NewMetadata().
Set("session_id", sessionID).
Set("model", "claude-3"),
}
type AuditFilter ¶
type AuditFilter struct {
// EventTypes limits results to specific event types.
// If empty, all event types are included.
EventTypes []string
// UserID limits results to events from a specific user.
// If empty, events from all users are included.
UserID string
// StartTime is the earliest event timestamp to include (inclusive).
// If zero, no lower bound is applied.
StartTime time.Time
// EndTime is the latest event timestamp to include (exclusive).
// If zero, no upper bound is applied.
EndTime time.Time
// ResourceType limits results to events involving specific resource types.
// If empty, all resource types are included.
ResourceType string
// ResourceID limits results to events involving a specific resource.
// If empty, all resources are included.
ResourceID string
// Outcome limits results to events with specific outcomes.
// If empty, all outcomes are included.
Outcome string
// Limit is the maximum number of events to return.
// If zero, implementation-specific default is used.
Limit int
// Offset is the number of events to skip (for pagination).
Offset int
}
AuditFilter defines criteria for querying audit events.
All fields are optional - only non-zero values are used as filters. Multiple fields are combined with AND logic.
Example:
// Find all failed auth events in the last hour
filter := AuditFilter{
EventTypes: []string{"auth.failed"},
StartTime: time.Now().Add(-time.Hour),
EndTime: time.Now(),
}
events, err := auditor.Query(ctx, filter)
type AuditLogger ¶
type AuditLogger interface {
// Log records a security-relevant event.
//
// Parameters:
// - ctx: Context for cancellation and timeout control
// - event: The audit event to record
//
// Returns:
// - error: nil on success, error if logging failed
//
// Implementations should:
// 1. Set Timestamp if zero
// 2. Validate required fields (EventType, UserID)
// 3. Persist or transmit the event
// 4. Return quickly (use async if needed)
Log(ctx context.Context, event AuditEvent) error
// Query retrieves audit events matching the filter criteria.
//
// Parameters:
// - ctx: Context for cancellation and timeout control
// - filter: Criteria for selecting events
//
// Returns:
// - []AuditEvent: Matching events, ordered by Timestamp descending
// - error: nil on success, error if query failed
//
// Note: NopAuditLogger returns empty slice (no events stored).
Query(ctx context.Context, filter AuditFilter) ([]AuditEvent, error)
// Flush ensures all buffered events are persisted.
//
// Call this before application shutdown to prevent event loss.
// For sync implementations, this may be a no-op.
//
// Parameters:
// - ctx: Context for cancellation and timeout control
//
// Returns:
// - error: nil on success, error if flush failed
Flush(ctx context.Context) error
}
AuditLogger records security-relevant events for compliance and analysis.
Implementations must be safe for concurrent use by multiple goroutines. The Log method should be non-blocking or have reasonable timeouts to avoid impacting application performance.
Open Source Behavior ¶
The default NopAuditLogger discards all events. This is appropriate for local single-user deployments where audit trails aren't required.
Enterprise Implementation ¶
Enterprise versions send events to SIEM systems (Splunk, Datadog, ELK), cloud logging (CloudWatch, Stackdriver), or compliance databases.
Example enterprise implementation:
type SplunkAuditLogger struct {
client *splunk.Client
index string
}
func (l *SplunkAuditLogger) Log(ctx context.Context, event AuditEvent) error {
if event.Timestamp.IsZero() {
event.Timestamp = time.Now().UTC()
}
return l.client.Index(ctx, l.index, event)
}
Async vs Sync Logging ¶
Implementations may choose sync or async logging:
- Sync: Blocks until event is persisted (safer, slower)
- Async: Returns immediately, buffers events (faster, may lose events)
For compliance-critical events, sync logging is recommended.
type AuditableRequest ¶
type AuditableRequest struct {
// Method is the HTTP method (GET, POST, etc.)
Method string
// Path is the request path (e.g., "/v1/chat/direct")
Path string
// Headers contains the HTTP request headers.
// Sensitive headers (Authorization) should be redacted by caller.
Headers HTTPHeaders
// Body is the raw request body bytes.
// This is what Enterprise will hash and potentially encrypt.
Body []byte
// UserID identifies who made the request.
// Extracted from AuthInfo by the handler.
UserID string
// SessionID is the conversation session identifier (if applicable).
SessionID string
// RequestID is the unique identifier for this request.
RequestID string
// Timestamp is when the request was received (always UTC).
Timestamp time.Time
}
AuditableRequest contains raw request data for audit capture.
This type is passed to CaptureRequest() to give Enterprise implementations access to the raw bytes for hashing, encryption, and storage. FOSS does NOT compute hashes - that's Enterprise's responsibility.
Usage ¶
Handlers create this struct with the raw request body and pass it to the RequestAuditor. Enterprise implementations then:
- Compute content_hash = SHA256(Body)
- Encrypt the body if required
- Store to immutable storage (GCS, QLDB, etc.)
Example:
req := &AuditableRequest{
Method: "POST",
Path: "/v1/chat/direct",
Headers: HTTPHeaders{"Content-Type": "application/json"},
Body: rawRequestBytes,
UserID: authInfo.UserID,
SessionID: sessionID,
RequestID: requestID,
Timestamp: time.Now().UTC(),
}
auditID, err := auditor.CaptureRequest(ctx, req)
type AuditableResponse ¶
type AuditableResponse struct {
// StatusCode is the HTTP response status code.
StatusCode int
// Headers contains the HTTP response headers.
Headers HTTPHeaders
// Body is the raw response body bytes.
// For streaming responses, this is all chunks concatenated.
Body []byte
// Timestamp is when the response was sent (always UTC).
Timestamp time.Time
}
AuditableResponse contains raw response data for audit capture.
This type is passed to CaptureResponse() to complete the audit record. The auditID from CaptureRequest() links the request and response together.
Streaming Responses ¶
For streaming endpoints (SSE), the handler should accumulate all chunks and pass the concatenated body to CaptureResponse() at the end of the stream.
Example:
resp := &AuditableResponse{
StatusCode: 200,
Headers: HTTPHeaders{"Content-Type": "application/json"},
Body: responseBytes,
Timestamp: time.Now().UTC(),
}
err := auditor.CaptureResponse(ctx, auditID, resp)
type AuthInfo ¶
type AuthInfo struct {
// UserID is the unique identifier for the authenticated user.
// This is the only required field and must never be empty.
UserID string
// Email is the user's email address.
// May be empty if not provided by the auth provider.
Email string
// Roles contains the user's role memberships for authorization decisions.
// Common roles: "admin", "analyst", "viewer", "auditor"
Roles []string
// Metadata holds additional claims from the identity provider.
// Enterprise implementations can store provider-specific data here
// without requiring changes to the core struct.
//
// Common metadata keys:
// - "groups": []string of group memberships
// - "department": organizational unit
// - "mfa_verified": whether MFA was used
// - "session_id": identity provider session ID
//
// Use NewMetadata() and type-safe accessors:
//
// Metadata: NewMetadata().
// Set("department", "engineering").
// Set("mfa_verified", true),
Metadata Metadata
}
AuthInfo contains identity information returned after successful authentication.
This struct is designed to be extensible via the Metadata field, allowing enterprise implementations to include additional claims without modifying the core type.
Required fields (always populated):
- UserID: Unique identifier for the user
Optional fields (may be empty):
- Email: User's email address
- Roles: List of roles/groups the user belongs to
- Metadata: Arbitrary key-value pairs for enterprise extensions
Example:
info := &AuthInfo{
UserID: "user-123",
Email: "user@example.com",
Roles: []string{"analyst", "viewer"},
Metadata: NewMetadata().
Set("department", "engineering").
Set("mfa_verified", true),
}
type AuthProvider ¶
type AuthProvider interface {
// Validate checks if the token is valid and returns the user's identity.
//
// Parameters:
// - ctx: Context for cancellation and timeout control
// - token: The authentication token (JWT, session ID, API key, etc.)
//
// Returns:
// - *AuthInfo: User identity information if valid
// - error: ErrUnauthorized (or wrapped) if invalid, other errors for failures
//
// The token format is implementation-specific:
// - JWT: "eyJhbGciOiJSUzI1NiIs..."
// - API Key: "ak_live_..."
// - Session: "sess_..."
Validate(ctx context.Context, token string) (*AuthInfo, error)
}
AuthProvider validates authentication tokens and returns user identity.
Implementations must be safe for concurrent use by multiple goroutines.
Open Source Behavior ¶
The default NopAuthProvider always returns a valid "local-user" with admin privileges. This allows the local CLI to function without any authentication infrastructure.
Enterprise Implementation ¶
Enterprise versions implement this interface to validate tokens against identity providers like Okta, Auth0, or Azure AD.
Example enterprise implementation:
type OktaAuthProvider struct {
client *okta.Client
}
func (p *OktaAuthProvider) Validate(ctx context.Context, token string) (*AuthInfo, error) {
claims, err := p.client.VerifyToken(ctx, token)
if err != nil {
return nil, fmt.Errorf("okta validation failed: %w", ErrUnauthorized)
}
return &AuthInfo{
UserID: claims.Subject,
Email: claims.Email,
Roles: claims.Groups,
}, nil
}
type AuthzProvider ¶
type AuthzProvider interface {
// Authorize checks if the user is permitted to perform the action.
//
// Parameters:
// - ctx: Context for cancellation and timeout control
// - req: The authorization request describing user, action, and resource
//
// Returns:
// - nil: Action is authorized
// - error: ErrUnauthorized (or wrapped) if denied
Authorize(ctx context.Context, req AuthzRequest) error
}
AuthzProvider checks if a user is authorized to perform an action.
Implementations must be safe for concurrent use by multiple goroutines.
Open Source Behavior ¶
The default NopAuthzProvider always allows all actions. This is appropriate for single-user local deployments where access control isn't needed.
Enterprise Implementation ¶
Enterprise versions implement RBAC, ABAC, or policy-based access control.
Example enterprise implementation:
type RBACProvider struct {
policies *PolicyEngine
}
func (p *RBACProvider) Authorize(ctx context.Context, req AuthzRequest) error {
allowed := p.policies.Check(req.User.Roles, req.Action, req.ResourceType)
if !allowed {
return fmt.Errorf("user %s cannot %s %s: %w",
req.User.UserID, req.Action, req.ResourceType, ErrUnauthorized)
}
return nil
}
type AuthzRequest ¶
type AuthzRequest struct {
// User is the authenticated user making the request.
// This comes from AuthProvider.Validate().
User *AuthInfo
// Action is the operation being attempted.
// Common actions: "create", "read", "update", "delete", "execute"
Action string
// ResourceType is the category of resource being accessed.
// Examples: "evaluation", "model", "session", "report"
ResourceType string
// ResourceID is the specific resource instance (optional).
// If empty, the check is for the resource type in general.
// Examples: "eval-123", "model-456"
ResourceID string
}
AuthzRequest describes an authorization check request.
This struct follows the common pattern of (subject, action, resource) for access control decisions.
Example:
req := AuthzRequest{
User: authInfo,
Action: "read",
ResourceType: "evaluation",
ResourceID: "eval-456",
}
err := authzProvider.Authorize(ctx, req)
type ChainVerificationResult ¶
type ChainVerificationResult struct {
// IsValid is true if the entire chain is intact.
IsValid bool
// TotalEntries is the number of entries verified.
TotalEntries int
// BreakPoint is the sequence number where integrity failed.
// Only meaningful when IsValid is false.
// Zero means the chain is valid or empty.
BreakPoint int
// ExpectedHash is what the hash should be at BreakPoint.
ExpectedHash string
// ActualHash is what the hash actually was at BreakPoint.
ActualHash string
// Message provides human-readable verification status.
Message string
}
ChainVerificationResult contains the outcome of hash chain verification.
Example:
result := auditor.VerifyChain(ctx, sessionID)
if !result.IsValid {
log.Error("chain integrity violation",
"break_point", result.BreakPoint,
"expected", result.ExpectedHash,
"actual", result.ActualHash,
)
}
type ClassificationFinding ¶
type ClassificationFinding struct {
// Classification is the sensitivity level of this finding.
Classification DataClassification
// Type describes what kind of data was found.
// Examples: "ssn", "credit_card", "email", "api_key", "password"
Type string
// Location describes where in the content the data was found.
// Format is implementation-specific (e.g., "line 5", "offset 100-120").
Location string
// Pattern identifies which detection rule matched.
// Useful for debugging and tuning classification rules.
// Examples: "ssn_regex", "credit_card_luhn", "api_key_entropy"
Pattern string
// Snippet is a truncated/redacted portion of the matched content.
// Used for audit logs without exposing full sensitive data.
// Should be safe to log (first/last few characters only).
Snippet string
}
ClassificationFinding describes a single piece of classified data.
Example:
finding := ClassificationFinding{
Classification: ClassificationPII,
Type: "email",
Location: "line 5, characters 10-30",
Pattern: "email_regex",
Snippet: "user@exa...", // Truncated for logging
}
type ClassificationResult ¶
type ClassificationResult struct {
// HighestLevel is the most sensitive classification found.
// Use this for quick policy decisions (e.g., block if SECRET).
HighestLevel DataClassification
// Findings lists all detected sensitive data with details.
// May be empty if nothing sensitive was found (HighestLevel == PUBLIC).
Findings []ClassificationFinding
// IsClean is true if no sensitive data was detected.
// Equivalent to HighestLevel == ClassificationPublic && len(Findings) == 0.
IsClean bool
}
ClassificationResult contains the outcome of data classification.
A single piece of data may contain multiple classifications (e.g., a document with both PII and confidential business information). The HighestLevel field provides a single value for quick policy decisions.
Example:
result := classifier.Classify(ctx, content)
if result.HighestLevel == ClassificationSecret {
log.Warn("secret data detected", "locations", result.Findings)
return errors.New("cannot process secret data")
}
type DataClassification ¶
type DataClassification string
DataClassification represents the sensitivity level of data.
Classifications follow common enterprise data handling policies and align with regulatory requirements (GDPR, HIPAA, CCPA). Higher levels require stricter handling controls.
Example:
switch classification {
case ClassificationSecret:
// Encrypt, audit access, restrict to need-to-know
case ClassificationPII:
// Redact in logs, apply retention policies
case ClassificationConfidential:
// Internal use only, no external sharing
case ClassificationPublic:
// Safe to share externally
}
const ( // ClassificationPublic indicates data that can be freely shared. // Examples: marketing materials, public documentation, open source code. ClassificationPublic DataClassification = "PUBLIC" // ClassificationConfidential indicates internal-only data. // Examples: internal memos, non-public financial data, strategy documents. ClassificationConfidential DataClassification = "CONFIDENTIAL" // ClassificationPII indicates personally identifiable information. // Examples: names, email addresses, phone numbers, IP addresses. // Requires special handling under GDPR, CCPA, and similar regulations. ClassificationPII DataClassification = "PII" // ClassificationSecret indicates highly sensitive data. // Examples: API keys, passwords, encryption keys, trade secrets. // Requires encryption at rest and in transit, strict access controls. ClassificationSecret DataClassification = "SECRET" )
type DataClassifier ¶
type DataClassifier interface {
// Classify analyzes content and returns its sensitivity classification.
//
// # Description
//
// Scans the provided content for patterns indicating sensitive data.
// Returns the highest classification found along with detailed findings.
//
// # Inputs
//
// - ctx: Context for cancellation and timeout control.
// - content: The text to classify. May be any length.
//
// # Outputs
//
// - *ClassificationResult: Classification details, never nil on success.
// - error: Non-nil if classification failed (e.g., timeout, invalid input).
//
// # Examples
//
// // Check a user message
// result, err := classifier.Classify(ctx, message)
// if result.HighestLevel >= ClassificationPII {
// // Apply PII handling procedures
// }
//
// # Limitations
//
// - Large content may be slow to process
// - Binary content is not supported
//
// # Assumptions
//
// - Content is valid UTF-8 text
// - Context deadline is respected for long operations
//
// # Thread Safety
//
// Safe to call concurrently from multiple goroutines.
Classify(ctx context.Context, content string) (*ClassificationResult, error)
// ClassifyBatch analyzes multiple content items efficiently.
//
// # Description
//
// Classifies multiple content items in a single call. Implementations
// may process items in parallel for better performance.
//
// # Inputs
//
// - ctx: Context for cancellation and timeout control.
// - contents: Slice of content items to classify.
//
// # Outputs
//
// - []*ClassificationResult: Results in same order as input.
// - error: Non-nil if any classification failed.
//
// # Examples
//
// contents := []string{message1, message2, message3}
// results, err := classifier.ClassifyBatch(ctx, contents)
// for i, result := range results {
// if !result.IsClean {
// log.Warn("sensitive data in item", "index", i)
// }
// }
//
// # Limitations
//
// - If one item fails, the entire batch may fail
// - Memory usage scales with batch size
//
// # Assumptions
//
// - All content items are valid UTF-8 text
// - Batch size is reasonable (implementation may limit)
//
// # Thread Safety
//
// Safe to call concurrently from multiple goroutines.
ClassifyBatch(ctx context.Context, contents []string) ([]*ClassificationResult, error)
}
DataClassifier scans data to determine its sensitivity classification.
Implementations must be safe for concurrent use by multiple goroutines.
Open Source Behavior ¶
The default NopDataClassifier always returns PUBLIC classification, indicating no sensitive data was detected. This allows the local CLI to function without classification infrastructure.
Enterprise Implementation ¶
Enterprise versions implement pattern-based detection using:
- Regular expressions for known formats (SSN, credit cards, etc.)
- Entropy analysis for secrets (API keys, passwords)
- Machine learning for context-aware PII detection
- Custom patterns for organization-specific data
Example enterprise implementation:
type RegexClassifier struct {
patterns map[DataClassification][]*regexp.Regexp
}
func (c *RegexClassifier) Classify(ctx context.Context, content string) (*ClassificationResult, error) {
var findings []ClassificationFinding
highest := ClassificationPublic
for level, patterns := range c.patterns {
for _, p := range patterns {
if matches := p.FindAllStringIndex(content, -1); matches != nil {
// Record findings, update highest level
}
}
}
return &ClassificationResult{
HighestLevel: highest,
Findings: findings,
IsClean: len(findings) == 0,
}, nil
}
Usage ¶
Classify data before storage, logging, or external transmission:
result, err := classifier.Classify(ctx, userMessage)
if err != nil {
return fmt.Errorf("classification failed: %w", err)
}
if result.HighestLevel == ClassificationSecret {
return errors.New("cannot process messages containing secrets")
}
// Log findings for compliance
for _, f := range result.Findings {
auditLogger.Log(ctx, AuditEvent{
EventType: "data.classified",
Metadata: map[string]any{
"classification": f.Classification,
"type": f.Type,
},
})
}
Limitations ¶
- Pattern-based detection has false positives/negatives
- Context matters: "123-45-6789" could be SSN or order number
- New data formats require pattern updates
Assumptions ¶
- Content is UTF-8 encoded text
- Classifications are hierarchical (SECRET > PII > CONFIDENTIAL > PUBLIC)
type Detection ¶
type Detection struct {
// Type categorizes what was detected.
// Common types: "ssn", "credit_card", "email", "phone", "api_key",
// "profanity", "pii", "secret", "prompt_injection"
Type string
// Location describes where in the message the item was found.
// Format is implementation-specific (e.g., "characters 10-20", "line 3")
Location string
// Action describes what was done with the detected item.
// Values: "redacted", "masked", "replaced", "blocked", "flagged"
Action string
// Original is the detected content (only populated in debug mode).
// WARNING: This may contain sensitive data - handle carefully.
Original string
// Replacement is what the content was replaced with (if Action is "replaced").
Replacement string
}
Detection describes a single item found by the filter.
Example:
detection := Detection{
Type: "credit_card",
Location: "characters 45-64",
Action: "redacted",
Original: "4111-1111-1111-1111", // Only in debug mode
}
type FilterResult ¶
type FilterResult struct {
// Original is the input message before filtering.
Original string
// Filtered is the message after filtering transformations.
// If WasModified is false, this equals Original.
Filtered string
// WasModified indicates if any transformations were applied.
WasModified bool
// WasBlocked indicates if the message was completely rejected.
// If true, Filtered should not be used.
WasBlocked bool
// BlockReason explains why the message was blocked (if WasBlocked).
BlockReason string
// Detections lists what the filter found in the message.
// Useful for audit logging and debugging.
Detections []Detection
}
FilterResult contains the outcome of a filter operation.
This struct provides detailed information about what the filter did, useful for debugging, audit trails, and user feedback.
Example:
result := FilterResult{
Original: "My SSN is 123-45-6789",
Filtered: "My SSN is [REDACTED]",
WasModified: true,
Detections: []Detection{
{Type: "SSN", Location: "position 10-21", Action: "redacted"},
},
}
type HTTPHeaders ¶
HTTPHeaders represents HTTP headers as a map.
Using a defined type provides clearer intent and allows future extension with helper methods if needed.
func (HTTPHeaders) Get ¶
func (h HTTPHeaders) Get(key string) string
Get retrieves a header value by key (case-sensitive).
func (HTTPHeaders) Set ¶
func (h HTTPHeaders) Set(key, value string)
Set adds or updates a header value.
type HashChainEntry ¶
type HashChainEntry struct {
// SessionID identifies the chain this entry belongs to.
// Each session has its own independent hash chain.
SessionID string
// SequenceNum is the position in the chain (1-indexed).
// Used to verify chain completeness and ordering.
SequenceNum int
// ContentHash is the hash of the content being recorded.
// For conversation turns: SHA256(question + answer)
// For requests: SHA256(request body)
ContentHash string
// PreviousHash is the ChainHash of the preceding entry.
// Empty string for the first entry in a chain (SequenceNum == 1).
PreviousHash string
// ChainHash is the cumulative hash incorporating all previous entries.
// ChainHash = SHA256(PreviousHash + ContentHash)
// This is the value stored and used for verification.
ChainHash string
// Timestamp is when this entry was created (always UTC).
Timestamp time.Time
// ContentType describes what kind of content was hashed.
// Examples: "conversation_turn", "request", "response", "document"
ContentType string
// Metadata contains additional context about the entry.
// May include: user_id, request_id, turn_number, etc.
//
// Use NewMetadata() and type-safe accessors:
//
// Metadata: NewMetadata().
// Set("user_id", userID).
// Set("request_id", requestID),
Metadata Metadata
}
HashChainEntry represents a single entry in a tamper-evident audit chain.
Hash chains provide cryptographic proof of the order and integrity of events. Each entry's hash incorporates the previous entry's hash, creating a chain that detects any modification to historical records.
Chain Structure ¶
Entry N hash = SHA256(Entry N-1 hash + Entry N content)
This ensures:
- Insertion detection: Adding entries breaks the chain
- Deletion detection: Removing entries breaks the chain
- Modification detection: Changing entries breaks the chain
Example:
entry := HashChainEntry{
SessionID: "sess-123",
SequenceNum: 5,
ContentHash: "abc123...",
PreviousHash: "def456...",
ChainHash: "ghi789...",
Timestamp: time.Now().UTC(),
ContentType: "conversation_turn",
Metadata: NewMetadata().
Set("user_id", "user-456").
Set("request_id", "req-789"),
}
type MessageFilter ¶
type MessageFilter interface {
// FilterInput processes a user message before LLM inference.
//
// Parameters:
// - ctx: Context for cancellation and timeout control
// - message: The raw user input
//
// Returns:
// - *FilterResult: The filtered message and metadata
// - error: Non-nil only for filter failures (not for blocks)
//
// If WasBlocked is true, the caller should:
// 1. Log the block via AuditLogger
// 2. Return ErrMessageBlocked to the user
// 3. NOT send the message to the LLM
FilterInput(ctx context.Context, message string) (*FilterResult, error)
// FilterOutput processes an LLM response before returning to user.
//
// Parameters:
// - ctx: Context for cancellation and timeout control
// - message: The LLM response
//
// Returns:
// - *FilterResult: The filtered response and metadata
// - error: Non-nil only for filter failures (not for blocks)
//
// Common output filtering:
// - Remove accidentally leaked API keys
// - Add compliance disclaimers
// - Mask generated PII
FilterOutput(ctx context.Context, message string) (*FilterResult, error)
// FilterContext processes context/system prompts before use.
//
// Parameters:
// - ctx: Context for cancellation and timeout control
// - context: System prompt or context being injected
//
// Returns:
// - *FilterResult: The filtered context and metadata
// - error: Non-nil only for filter failures
//
// This is called when injecting retrieved documents (RAG),
// system prompts, or other context into the conversation.
FilterContext(ctx context.Context, contextMsg string) (*FilterResult, error)
}
MessageFilter transforms messages before and after LLM processing.
Implementations must be safe for concurrent use by multiple goroutines.
Filter Pipeline ¶
Messages flow through filters at two points:
FilterInput: Before sending to LLM - Remove PII from user messages - Block policy violations - Detect prompt injection attempts
FilterOutput: Before returning to user - Remove leaked secrets from responses - Add compliance disclaimers - Mask sensitive generated content
Open Source Behavior ¶
The default NopMessageFilter passes all messages through unchanged. This is appropriate for local single-user deployments where content filtering isn't required.
Enterprise Implementation ¶
Enterprise versions implement content policies, PII detection, and compliance requirements.
Example enterprise implementation:
type PIIFilter struct {
patterns []PIIPattern
policy *Policy
}
func (f *PIIFilter) FilterInput(ctx context.Context, msg string) (*FilterResult, error) {
result := &FilterResult{Original: msg, Filtered: msg}
for _, pattern := range f.patterns {
if matches := pattern.FindAll(msg); len(matches) > 0 {
result.Filtered = pattern.Redact(result.Filtered)
result.WasModified = true
result.Detections = append(result.Detections, Detection{
Type: pattern.Name,
Action: "redacted",
})
}
}
return result, nil
}
Blocking vs Transforming ¶
Filters can either:
- Transform: Modify content and allow it through (e.g., redact SSN)
- Block: Reject the entire message (e.g., policy violation)
To block, return a FilterResult with WasBlocked=true and BlockReason set. The caller should then return ErrMessageBlocked to the user.
type Metadata ¶
Metadata stores arbitrary key-value pairs for context and logging.
Using a defined type rather than map[string]any provides:
- Clearer intent in function signatures
- Ability to add methods for type-safe access
- Compile-time distinction from arbitrary maps
- Self-documenting code
Common Keys ¶
While Metadata is flexible, these keys are commonly used:
- "session_id": Conversation session identifier
- "request_id": Request correlation ID
- "user_id": User performing the action
- "model": AI model used
- "error": Error message if applicable
- "ip_address": Client IP address
- "user_agent": Client identifier
- "duration_ms": Operation duration
- "turn_number": Conversation turn count
Thread Safety ¶
Metadata is NOT thread-safe. Do not share a single Metadata instance across goroutines without external synchronization.
Example:
meta := extensions.NewMetadata().
Set("session_id", sessionID).
Set("model", "claude-3").
Set("duration_ms", 150)
// Type-safe access
if sessionID, ok := meta.GetString("session_id"); ok {
log.Info("session", "id", sessionID)
}
func NewMetadata ¶
func NewMetadata() Metadata
NewMetadata creates an empty Metadata instance.
Description ¶
Returns an initialized Metadata map ready for use. This is the preferred way to create Metadata instances.
Outputs ¶
- Metadata: An empty, initialized map.
Examples ¶
meta := NewMetadata() meta["key"] = "value"
Thread Safety ¶
The returned Metadata is not thread-safe.
func (Metadata) Clone ¶
Clone creates a shallow copy of the Metadata.
Description ¶
Creates a new Metadata instance with the same key-value pairs. Note: Values themselves are not deep-copied.
Outputs ¶
- Metadata: A new instance with copied key-value pairs.
Examples ¶
original := NewMetadata().Set("key", "value")
copy := original.Clone()
copy.Set("key", "modified") // original unchanged
Limitations ¶
This is a shallow copy. If values are pointers or references, they will point to the same underlying data.
Thread Safety ¶
The returned copy is independent but not thread-safe.
func (Metadata) Delete ¶
Delete removes a key from the Metadata.
Description ¶
Removes the specified key and its value. Safe to call even if the key doesn't exist.
Inputs ¶
- key: The metadata key to delete.
Outputs ¶
- Metadata: The same instance (for chaining).
Examples ¶
meta.Delete("sensitive_data")
Thread Safety ¶
Not thread-safe. Do not call concurrently.
func (Metadata) Get ¶
Get retrieves a value by key.
Description ¶
Returns the value associated with the key and a boolean indicating whether the key exists.
Inputs ¶
- key: The metadata key to retrieve.
Outputs ¶
- any: The value, or nil if not found.
- bool: True if the key exists, false otherwise.
Examples ¶
if value, ok := meta.Get("session_id"); ok {
// value exists
}
Thread Safety ¶
Not thread-safe if modified concurrently.
func (Metadata) GetBool ¶
GetBool retrieves a bool value by key.
Description ¶
Type-safe accessor for bool values. Returns the bool and true if the key exists and the value is a bool, otherwise returns false and false.
Inputs ¶
- key: The metadata key to retrieve.
Outputs ¶
- bool: The value, or false if not found or not a bool.
- bool: True if the key exists and value is a bool.
Examples ¶
if isAdmin, ok := meta.GetBool("is_admin"); ok && isAdmin {
// User is admin
}
Thread Safety ¶
Not thread-safe if modified concurrently.
func (Metadata) GetFloat64 ¶
GetFloat64 retrieves a float64 value by key.
Description ¶
Type-safe accessor for float64 values. Returns the float64 and true if the key exists and the value is a float64, otherwise returns 0 and false.
Inputs ¶
- key: The metadata key to retrieve.
Outputs ¶
- float64: The value, or 0 if not found or not a float64.
- bool: True if the key exists and value is a float64.
Examples ¶
if score, ok := meta.GetFloat64("confidence_score"); ok {
fmt.Printf("Confidence: %.2f\n", score)
}
Thread Safety ¶
Not thread-safe if modified concurrently.
func (Metadata) GetInt ¶
GetInt retrieves an int value by key.
Description ¶
Type-safe accessor for int values. Returns the int and true if the key exists and the value is an int, otherwise returns 0 and false.
Inputs ¶
- key: The metadata key to retrieve.
Outputs ¶
- int: The value, or 0 if not found or not an int.
- bool: True if the key exists and value is an int.
Examples ¶
if turnNum, ok := meta.GetInt("turn_number"); ok {
fmt.Printf("Turn %d\n", turnNum)
}
Thread Safety ¶
Not thread-safe if modified concurrently.
func (Metadata) GetInt64 ¶
GetInt64 retrieves an int64 value by key.
Description ¶
Type-safe accessor for int64 values. Returns the int64 and true if the key exists and the value is an int64, otherwise returns 0 and false.
Inputs ¶
- key: The metadata key to retrieve.
Outputs ¶
- int64: The value, or 0 if not found or not an int64.
- bool: True if the key exists and value is an int64.
Examples ¶
if durationMs, ok := meta.GetInt64("duration_ms"); ok {
fmt.Printf("Duration: %dms\n", durationMs)
}
Thread Safety ¶
Not thread-safe if modified concurrently.
func (Metadata) GetString ¶
GetString retrieves a string value by key.
Description ¶
Type-safe accessor for string values. Returns the string and true if the key exists and the value is a string, otherwise returns empty string and false.
Inputs ¶
- key: The metadata key to retrieve.
Outputs ¶
- string: The value, or "" if not found or not a string.
- bool: True if the key exists and value is a string.
Examples ¶
if sessionID, ok := meta.GetString("session_id"); ok {
log.Info("session", "id", sessionID)
}
Thread Safety ¶
Not thread-safe if modified concurrently.
func (Metadata) GetTime ¶
GetTime retrieves a time.Time value by key.
Description ¶
Type-safe accessor for time.Time values. Returns the time and true if the key exists and the value is a time.Time, otherwise returns zero time and false.
Inputs ¶
- key: The metadata key to retrieve.
Outputs ¶
- time.Time: The value, or zero time if not found or not a time.Time.
- bool: True if the key exists and value is a time.Time.
Examples ¶
if timestamp, ok := meta.GetTime("created_at"); ok {
fmt.Printf("Created: %s\n", timestamp.Format(time.RFC3339))
}
Thread Safety ¶
Not thread-safe if modified concurrently.
func (Metadata) Has ¶
Has checks if a key exists in the Metadata.
Description ¶
Returns true if the key exists, regardless of its value (including nil).
Inputs ¶
- key: The metadata key to check.
Outputs ¶
- bool: True if the key exists.
Examples ¶
if meta.Has("error") {
// Handle error case
}
Thread Safety ¶
Not thread-safe if modified concurrently.
func (Metadata) Keys ¶
Keys returns all keys in the Metadata.
Description ¶
Returns a slice of all keys. Order is not guaranteed.
Outputs ¶
- []string: All keys in the Metadata.
Examples ¶
keys := meta.Keys()
for _, key := range keys {
fmt.Printf("Key: %s\n", key)
}
Thread Safety ¶
Not thread-safe if modified concurrently.
func (Metadata) Len ¶
Len returns the number of key-value pairs.
Description ¶
Returns the count of entries in the Metadata.
Outputs ¶
- int: Number of key-value pairs.
Examples ¶
if meta.Len() == 0 {
fmt.Println("No metadata")
}
Thread Safety ¶
Not thread-safe if modified concurrently.
func (Metadata) Merge ¶
Merge copies all key-value pairs from another Metadata into this one.
Description ¶
Adds all entries from the other Metadata. Existing keys are overwritten.
Inputs ¶
- other: The Metadata to merge from. Can be nil (no-op).
Outputs ¶
- Metadata: The same instance (for chaining).
Examples ¶
base := NewMetadata().Set("env", "prod")
extra := NewMetadata().Set("version", "1.0")
base.Merge(extra)
// base now has both "env" and "version"
Thread Safety ¶
Not thread-safe. Do not call concurrently.
func (Metadata) Set ¶
Set adds or updates a key-value pair and returns the Metadata for chaining.
Description ¶
Provides a fluent interface for building Metadata instances. Returns the same Metadata instance for method chaining.
Inputs ¶
- key: The metadata key (string).
- value: The metadata value (any type).
Outputs ¶
- Metadata: The same instance (for chaining).
Examples ¶
meta := NewMetadata().
Set("session_id", "sess-123").
Set("user_id", "user-456").
Set("timestamp", time.Now())
Thread Safety ¶
Not thread-safe. Do not call concurrently.
type NopAuditLogger ¶
type NopAuditLogger struct{}
NopAuditLogger is the default audit logger for open source.
It discards all events without recording them. This is appropriate for local single-user deployments where audit trails aren't required.
Thread-safe: This implementation has no mutable state.
Example:
logger := &NopAuditLogger{}
err := logger.Log(ctx, AuditEvent{
EventType: "chat.message",
UserID: "local-user",
})
// err == nil (event discarded)
events, err := logger.Query(ctx, AuditFilter{})
// events == []AuditEvent{} (always empty)
func (*NopAuditLogger) Flush ¶
func (l *NopAuditLogger) Flush(_ context.Context) error
Flush is a no-op since nothing is buffered.
Always returns nil (success).
func (*NopAuditLogger) Log ¶
func (l *NopAuditLogger) Log(_ context.Context, _ AuditEvent) error
Log discards the event without recording it.
Always returns nil (success) regardless of event content.
func (*NopAuditLogger) Query ¶
func (l *NopAuditLogger) Query(_ context.Context, _ AuditFilter) ([]AuditEvent, error)
Query returns an empty slice (no events are stored).
Always returns nil error with empty results.
type NopAuthProvider ¶
type NopAuthProvider struct{}
NopAuthProvider is the default authentication provider for open source.
It always returns a valid local user with admin privileges, enabling the CLI to function without any authentication infrastructure.
Thread-safe: This implementation has no mutable state.
Example:
provider := &NopAuthProvider{}
info, err := provider.Validate(ctx, "any-token")
// info.UserID == "local-user"
// info.Roles == []string{"admin"}
// err == nil
type NopAuthzProvider ¶
type NopAuthzProvider struct{}
NopAuthzProvider is the default authorization provider for open source.
It always allows all actions, enabling the CLI to function without any access control infrastructure.
Thread-safe: This implementation has no mutable state.
Example:
provider := &NopAuthzProvider{}
err := provider.Authorize(ctx, AuthzRequest{
User: &AuthInfo{UserID: "anyone"},
Action: "delete",
ResourceType: "everything",
})
// err == nil (always allowed)
func (*NopAuthzProvider) Authorize ¶
func (p *NopAuthzProvider) Authorize(_ context.Context, _ AuthzRequest) error
Authorize always returns nil, allowing all actions.
The request parameter is ignored - all actions are permitted. This is intentional for local single-user deployments where access control isn't needed.
type NopDataClassifier ¶
type NopDataClassifier struct{}
NopDataClassifier is the default classifier for open source.
It always returns PUBLIC classification with no findings, indicating no sensitive data was detected. This allows the CLI to function without classification infrastructure.
Thread-safe: This implementation has no mutable state.
Example:
classifier := &NopDataClassifier{}
result, err := classifier.Classify(ctx, "user SSN: 123-45-6789")
// result.HighestLevel == ClassificationPublic
// result.IsClean == true
// result.Findings == nil
// err == nil
func (*NopDataClassifier) Classify ¶
func (c *NopDataClassifier) Classify(_ context.Context, _ string) (*ClassificationResult, error)
Classify always returns PUBLIC classification with no findings.
Description ¶
Returns a clean classification result regardless of content. This is intentional for local single-user deployments where data classification isn't required.
Inputs ¶
- ctx: Ignored (no external calls).
- content: Ignored (not analyzed).
Outputs ¶
- *ClassificationResult: Always PUBLIC, IsClean=true, no findings.
- error: Always nil.
Examples ¶
result, _ := classifier.Classify(ctx, "any content") // result.IsClean == true
Limitations ¶
- Does not detect any sensitive data.
Assumptions ¶
- Caller accepts that no classification is performed.
Thread Safety ¶
Safe to call concurrently (stateless).
func (*NopDataClassifier) ClassifyBatch ¶
func (c *NopDataClassifier) ClassifyBatch(_ context.Context, contents []string) ([]*ClassificationResult, error)
ClassifyBatch always returns PUBLIC classification for all items.
Description ¶
Returns clean classification results for all content items. This is intentional for local deployments without classification.
Inputs ¶
- ctx: Ignored (no external calls).
- contents: Used only to determine result slice length.
Outputs ¶
- []*ClassificationResult: All PUBLIC, IsClean=true, same length as input.
- error: Always nil.
Examples ¶
results, _ := classifier.ClassifyBatch(ctx, []string{"a", "b", "c"})
// len(results) == 3
// all results.IsClean == true
Limitations ¶
- Does not detect any sensitive data.
Assumptions ¶
- Caller accepts that no classification is performed.
Thread Safety ¶
Safe to call concurrently (stateless).
type NopMessageFilter ¶
type NopMessageFilter struct{}
NopMessageFilter is the default message filter for open source.
It passes all messages through unchanged without any transformation or blocking. This is appropriate for local single-user deployments where content filtering isn't required.
Thread-safe: This implementation has no mutable state.
Example:
filter := &NopMessageFilter{}
result, err := filter.FilterInput(ctx, "My SSN is 123-45-6789")
// result.Filtered == "My SSN is 123-45-6789" (unchanged)
// result.WasModified == false
// err == nil
func (*NopMessageFilter) FilterContext ¶
func (f *NopMessageFilter) FilterContext(ctx context.Context, contextMsg string) (*FilterResult, error)
FilterContext returns the context unchanged.
No transformations or blocking are applied.
func (*NopMessageFilter) FilterInput ¶
func (f *NopMessageFilter) FilterInput(ctx context.Context, message string) (*FilterResult, error)
FilterInput returns the message unchanged.
No transformations or blocking are applied.
func (*NopMessageFilter) FilterOutput ¶
func (f *NopMessageFilter) FilterOutput(ctx context.Context, message string) (*FilterResult, error)
FilterOutput returns the message unchanged.
No transformations or blocking are applied.
type NopRequestAuditor ¶
type NopRequestAuditor struct{}
NopRequestAuditor is the default auditor for open source.
It accepts all operations without persisting anything. This allows the CLI to function without cryptographic audit infrastructure. Enterprise implementations replace this with actual storage.
Thread-safe: This implementation has no mutable state (discards everything).
Example:
auditor := &NopRequestAuditor{}
auditID, _ := auditor.CaptureRequest(ctx, req)
// auditID == "" (no tracking)
auditor.CaptureResponse(ctx, auditID, resp)
// No-op, nothing stored
func (*NopRequestAuditor) CaptureRequest ¶
func (a *NopRequestAuditor) CaptureRequest(_ context.Context, _ *AuditableRequest) (string, error)
CaptureRequest accepts the request without storing it.
Description ¶
Always succeeds and returns empty auditID. This is intentional for local deployments without audit requirements. Enterprise implementations would store the request and return a tracking ID.
Inputs ¶
- ctx: Ignored (no external calls).
- req: Ignored (not stored).
Outputs ¶
- string: Always empty string (no tracking).
- error: Always nil.
Examples ¶
auditID, err := auditor.CaptureRequest(ctx, req) // auditID == "" // err == nil
Limitations ¶
- Does not store request data.
- No audit trail maintained.
Assumptions ¶
- Caller accepts that no audit is performed.
Thread Safety ¶
Safe to call concurrently (stateless).
func (*NopRequestAuditor) CaptureResponse ¶
func (a *NopRequestAuditor) CaptureResponse(_ context.Context, _ string, _ *AuditableResponse) error
CaptureResponse accepts the response without storing it.
Description ¶
Always succeeds without storing anything. This is intentional for local deployments without audit requirements.
Inputs ¶
- ctx: Ignored (no external calls).
- auditID: Ignored (no tracking).
- resp: Ignored (not stored).
Outputs ¶
- error: Always nil.
Examples ¶
err := auditor.CaptureResponse(ctx, "", resp) // err == nil
Limitations ¶
- Does not store response data.
Assumptions ¶
- Caller accepts that no audit is performed.
Thread Safety ¶
Safe to call concurrently (stateless).
func (*NopRequestAuditor) GetChainLength ¶
GetChainLength always returns zero.
Description ¶
Returns zero since no entries are persisted.
Inputs ¶
- ctx: Ignored (no external calls).
- sessionID: Ignored (no entries exist).
Outputs ¶
- int: Always 0.
- error: Always nil.
Examples ¶
count, err := auditor.GetChainLength(ctx, "any-session") // count == 0 // err == nil
Limitations ¶
- Always returns zero (no persistence).
Assumptions ¶
- Caller accepts that chains are not tracked.
Thread Safety ¶
Safe to call concurrently (stateless).
func (*NopRequestAuditor) GetLastEntry ¶
func (a *NopRequestAuditor) GetLastEntry(_ context.Context, _ string) (*HashChainEntry, error)
GetLastEntry returns nil, indicating an empty chain.
Description ¶
Always returns nil since no entries are persisted.
Inputs ¶
- ctx: Ignored (no external calls).
- sessionID: Ignored (no entries exist).
Outputs ¶
- *HashChainEntry: Always nil.
- error: Always nil.
Examples ¶
entry, err := auditor.GetLastEntry(ctx, "any-session") // entry == nil // err == nil
Limitations ¶
- Always returns nil (no persistence).
Assumptions ¶
- Caller handles nil entry appropriately.
Thread Safety ¶
Safe to call concurrently (stateless).
func (*NopRequestAuditor) RecordEntry ¶
func (a *NopRequestAuditor) RecordEntry(_ context.Context, _ HashChainEntry) error
RecordEntry accepts the entry without persisting it.
Description ¶
Always succeeds without storing anything. This is intentional for local deployments without audit requirements.
Inputs ¶
- ctx: Ignored (no external calls).
- entry: Ignored (not persisted).
Outputs ¶
- error: Always nil.
Examples ¶
err := auditor.RecordEntry(ctx, entry) // err == nil
Limitations ¶
- Does not persist entries.
- No tamper detection capability.
Assumptions ¶
- Caller accepts that no audit trail is maintained.
Thread Safety ¶
Safe to call concurrently (stateless).
func (*NopRequestAuditor) VerifyChain ¶
func (a *NopRequestAuditor) VerifyChain(_ context.Context, _ string) (*ChainVerificationResult, error)
VerifyChain always returns valid.
Description ¶
Returns a valid result since no entries are tracked. This is intentional for local deployments.
Inputs ¶
- ctx: Ignored (no external calls).
- sessionID: Ignored (no entries exist).
Outputs ¶
- *ChainVerificationResult: Always valid with zero entries.
- error: Always nil.
Examples ¶
result, err := auditor.VerifyChain(ctx, "any-session") // result.IsValid == true // result.TotalEntries == 0
Limitations ¶
- No actual verification performed.
Assumptions ¶
- Caller accepts that chains are not tracked.
Thread Safety ¶
Safe to call concurrently (stateless).
type RequestAuditor ¶
type RequestAuditor interface {
// CaptureRequest records the raw request for audit purposes.
//
// # Description
//
// Called at the START of request processing with the raw request body.
// Enterprise implementations receive the raw bytes to:
// 1. Compute content_hash = SHA256(Body)
// 2. Encrypt the body if required
// 3. Store to immutable storage (GCS, QLDB, etc.)
//
// Returns an auditID that must be passed to CaptureResponse to link them.
//
// # Inputs
//
// - ctx: Context for cancellation and timeout control.
// - req: Raw request data including body bytes.
//
// # Outputs
//
// - string: Audit ID to pass to CaptureResponse. Empty for NopRequestAuditor.
// - error: Non-nil if capture failed.
//
// # Examples
//
// auditID, err := auditor.CaptureRequest(ctx, &AuditableRequest{
// Method: c.Request.Method,
// Path: c.Request.URL.Path,
// Body: requestBody,
// UserID: authInfo.UserID,
// RequestID: req.RequestID,
// Timestamp: time.Now().UTC(),
// })
// if err != nil {
// log.Error("audit capture failed", "error", err)
// }
// // ... process request ...
// auditor.CaptureResponse(ctx, auditID, responseData)
//
// # Limitations
//
// - Caller must preserve auditID to call CaptureResponse.
// - Request body should be read before calling (use io.TeeReader).
//
// # Assumptions
//
// - Body contains the complete request payload.
// - Sensitive headers are redacted by caller if needed.
//
// # Thread Safety
//
// Safe to call concurrently.
CaptureRequest(ctx context.Context, req *AuditableRequest) (auditID string, err error)
// CaptureResponse records the raw response for audit purposes.
//
// # Description
//
// Called at the END of request processing with the raw response body.
// The auditID links this response to its corresponding request.
// Enterprise implementations receive the raw bytes to hash and store.
//
// For streaming endpoints, accumulate all chunks and call this once at the end.
//
// # Inputs
//
// - ctx: Context for cancellation and timeout control.
// - auditID: The ID returned from CaptureRequest.
// - resp: Raw response data including body bytes.
//
// # Outputs
//
// - error: Non-nil if capture failed.
//
// # Examples
//
// err := auditor.CaptureResponse(ctx, auditID, &AuditableResponse{
// StatusCode: 200,
// Body: responseBytes,
// Timestamp: time.Now().UTC(),
// })
//
// # Limitations
//
// - Must be called with valid auditID from CaptureRequest.
// - For streaming, caller must accumulate all chunks.
//
// # Assumptions
//
// - Body contains the complete response payload.
// - auditID is valid and corresponds to a captured request.
//
// # Thread Safety
//
// Safe to call concurrently.
CaptureResponse(ctx context.Context, auditID string, resp *AuditableResponse) error
// RecordEntry adds a new entry to the hash chain.
//
// # Description
//
// Persists an audit entry and updates the chain hash. Implementations
// should verify chain continuity before accepting the entry.
//
// # Inputs
//
// - ctx: Context for cancellation and timeout control.
// - entry: The hash chain entry to record.
//
// # Outputs
//
// - error: Non-nil if recording failed or chain continuity was violated.
//
// # Examples
//
// err := auditor.RecordEntry(ctx, HashChainEntry{
// SessionID: "sess-123",
// SequenceNum: 1,
// ContentHash: "abc...",
// ContentType: "conversation_turn",
// })
//
// # Limitations
//
// - Entry cannot be modified after recording.
// - Requires PreviousHash for SequenceNum > 1.
//
// # Assumptions
//
// - ContentHash is correctly computed by caller.
// - SequenceNum is monotonically increasing per session.
//
// # Thread Safety
//
// Safe to call concurrently, but entries for the same session
// should be serialized to maintain chain order.
RecordEntry(ctx context.Context, entry HashChainEntry) error
// GetLastEntry retrieves the most recent entry for a session.
//
// # Description
//
// Returns the last recorded entry, which is needed to compute
// PreviousHash for the next entry in the chain.
//
// # Inputs
//
// - ctx: Context for cancellation and timeout control.
// - sessionID: The session to query.
//
// # Outputs
//
// - *HashChainEntry: The last entry, or nil if chain is empty.
// - error: Non-nil if retrieval failed.
//
// # Examples
//
// lastEntry, err := auditor.GetLastEntry(ctx, sessionID)
// if lastEntry != nil {
// newEntry.PreviousHash = lastEntry.ChainHash
// newEntry.SequenceNum = lastEntry.SequenceNum + 1
// } else {
// newEntry.PreviousHash = ""
// newEntry.SequenceNum = 1
// }
//
// # Limitations
//
// - Returns nil for non-existent sessions (not an error).
//
// # Assumptions
//
// - Session IDs are unique across the system.
//
// # Thread Safety
//
// Safe to call concurrently.
GetLastEntry(ctx context.Context, sessionID string) (*HashChainEntry, error)
// VerifyChain validates the integrity of a session's hash chain.
//
// # Description
//
// Retrieves all entries for a session and verifies that each entry's
// ChainHash correctly incorporates the previous entry's hash.
//
// # Inputs
//
// - ctx: Context for cancellation and timeout control.
// - sessionID: The session to verify.
//
// # Outputs
//
// - *ChainVerificationResult: Verification outcome with details.
// - error: Non-nil if verification could not be performed.
//
// # Examples
//
// result, err := auditor.VerifyChain(ctx, sessionID)
// if err != nil {
// return fmt.Errorf("verification failed: %w", err)
// }
// if !result.IsValid {
// alertSecurityTeam(sessionID, result.BreakPoint)
// }
//
// # Limitations
//
// - Requires loading all entries (may be slow for long chains).
// - Empty chains are considered valid.
//
// # Assumptions
//
// - All entries for the session are retrievable.
// - Hash algorithm matches what was used during recording.
//
// # Thread Safety
//
// Safe to call concurrently.
VerifyChain(ctx context.Context, sessionID string) (*ChainVerificationResult, error)
// GetChainLength returns the number of entries in a session's chain.
//
// # Description
//
// Returns the count of recorded entries without loading them all.
// Useful for quick checks and display purposes.
//
// # Inputs
//
// - ctx: Context for cancellation and timeout control.
// - sessionID: The session to query.
//
// # Outputs
//
// - int: Number of entries (0 for non-existent sessions).
// - error: Non-nil if count could not be retrieved.
//
// # Examples
//
// count, err := auditor.GetChainLength(ctx, sessionID)
// fmt.Printf("Session has %d recorded turns\n", count)
//
// # Limitations
//
// - Does not verify chain integrity.
//
// # Assumptions
//
// - Count is consistent with actual entries.
//
// # Thread Safety
//
// Safe to call concurrently.
GetChainLength(ctx context.Context, sessionID string) (int, error)
}
RequestAuditor provides tamper-evident audit logging via hash chains.
Implementations must be safe for concurrent use by multiple goroutines.
Open Source Behavior ¶
The default NopRequestAuditor accepts all entries and always reports chains as valid. This allows the local CLI to function without cryptographic audit infrastructure.
Enterprise Implementation ¶
Enterprise versions implement persistent hash chains stored in:
- Append-only databases (e.g., Amazon QLDB)
- Immutable storage (e.g., S3 Object Lock)
- Blockchain-based ledgers
- Hardware security modules (HSMs)
Example enterprise implementation:
type QLDBRequestAuditor struct {
ledger *qldb.Driver
}
func (a *QLDBRequestAuditor) RecordEntry(ctx context.Context, entry HashChainEntry) error {
// Verify chain continuity
lastHash, err := a.getLastHash(ctx, entry.SessionID)
if err != nil {
return err
}
if entry.PreviousHash != lastHash {
return errors.New("chain continuity violation")
}
// Persist to QLDB (immutable)
return a.ledger.Execute(ctx, func(txn qldb.Transaction) error {
return txn.Insert("audit_chain", entry)
})
}
Usage ¶
Record entries when processing requests:
// After computing response hash
entry := HashChainEntry{
SessionID: sessionID,
SequenceNum: turnNumber,
ContentHash: responseHash,
// PreviousHash filled by implementation or caller
Timestamp: time.Now().UTC(),
ContentType: "conversation_turn",
Metadata: NewMetadata().
Set("user_id", userID).
Set("request_id", requestID),
}
if err := auditor.RecordEntry(ctx, entry); err != nil {
log.Error("audit recording failed", "error", err)
// Consider failing the request for compliance
}
Regulatory Compliance ¶
Hash chains support:
- HIPAA: Audit controls (§164.312(b))
- SOX: Internal controls over financial reporting
- GDPR: Accountability and records of processing
- PCI DSS: Logging and monitoring (Requirement 10)
Limitations ¶
- Cannot prevent real-time tampering (only detect after the fact)
- Chain verification requires all entries (no partial verification)
- Storage grows linearly with entries
Assumptions ¶
- Clock synchronization across nodes (for timestamp ordering)
- SHA256 is collision-resistant (standard assumption)
- Enterprise storage is truly append-only
type ServiceOptions ¶
type ServiceOptions struct {
// AuthProvider validates authentication tokens.
// Default: NopAuthProvider (always returns valid local user)
AuthProvider AuthProvider
// AuthzProvider checks authorization permissions.
// Default: NopAuthzProvider (always allows all actions)
AuthzProvider AuthzProvider
// AuditLogger records security-relevant events.
// Default: NopAuditLogger (discards all events)
AuditLogger AuditLogger
// MessageFilter transforms messages before/after processing.
// Default: NopMessageFilter (passes through unchanged)
MessageFilter MessageFilter
// DataClassifier determines data sensitivity classification.
// Default: NopDataClassifier (always returns PUBLIC)
DataClassifier DataClassifier
// RequestAuditor provides tamper-evident hash chain logging.
// Default: NopRequestAuditor (discards entries, always valid)
RequestAuditor RequestAuditor
}
ServiceOptions groups all extension points for service configuration.
Pass this to service constructors to enable enterprise features. All fields are optional; nil values are replaced with no-op defaults when DefaultOptions() is called or when services check for nil.
Example:
// Open source: use defaults
opts := extensions.DefaultOptions()
// Enterprise: inject implementations
opts := extensions.ServiceOptions{
AuthProvider: oktaProvider,
AuditLogger: splunkAuditor,
MessageFilter: piiFilter,
}
func DefaultOptions ¶
func DefaultOptions() ServiceOptions
DefaultOptions returns ServiceOptions with no-op defaults.
This is the configuration used by the open source version. All operations are allowed, no audit trail, no filtering.
Returns:
- ServiceOptions with all fields set to no-op implementations
func (ServiceOptions) WithAudit ¶
func (opts ServiceOptions) WithAudit(logger AuditLogger) ServiceOptions
WithAudit returns a copy of opts with the given AuditLogger.
func (ServiceOptions) WithAuth ¶
func (opts ServiceOptions) WithAuth(provider AuthProvider) ServiceOptions
WithAuth returns a copy of opts with the given AuthProvider. Useful for fluent configuration.
func (ServiceOptions) WithAuthz ¶
func (opts ServiceOptions) WithAuthz(provider AuthzProvider) ServiceOptions
WithAuthz returns a copy of opts with the given AuthzProvider.
func (ServiceOptions) WithClassifier ¶
func (opts ServiceOptions) WithClassifier(classifier DataClassifier) ServiceOptions
WithClassifier returns a copy of opts with the given DataClassifier.
func (ServiceOptions) WithFilter ¶
func (opts ServiceOptions) WithFilter(filter MessageFilter) ServiceOptions
WithFilter returns a copy of opts with the given MessageFilter.
func (ServiceOptions) WithRequestAuditor ¶
func (opts ServiceOptions) WithRequestAuditor(auditor RequestAuditor) ServiceOptions
WithRequestAuditor returns a copy of opts with the given RequestAuditor.