extensions

package
v1.0.0 Latest Latest
Warning

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

Go to latest
Published: Mar 11, 2026 License: AGPL-3.0 Imports: 3 Imported by: 0

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

Constants

This section is empty.

Variables

View Source
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)
}
View Source
var ErrUnauthorized = errors.New("unauthorized")

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:

  1. Compute content_hash = SHA256(Body)
  2. Encrypt the body if required
  3. 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),
}

func (*AuthInfo) HasRole

func (a *AuthInfo) HasRole(role string) bool

HasRole checks if the user has a specific role.

This is a convenience method for authorization checks:

if !authInfo.HasRole("admin") {
    return ErrUnauthorized
}

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

type HTTPHeaders map[string]string

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:

  1. FilterInput: Before sending to LLM - Remove PII from user messages - Block policy violations - Detect prompt injection attempts

  2. 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

type Metadata map[string]any

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

func (m Metadata) Clone() Metadata

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

func (m Metadata) Delete(key string) Metadata

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

func (m Metadata) Get(key string) (any, bool)

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

func (m Metadata) GetBool(key string) (bool, bool)

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

func (m Metadata) GetFloat64(key string) (float64, bool)

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

func (m Metadata) GetInt(key string) (int, bool)

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

func (m Metadata) GetInt64(key string) (int64, bool)

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

func (m Metadata) GetString(key string) (string, bool)

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

func (m Metadata) GetTime(key string) (time.Time, bool)

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

func (m Metadata) Has(key string) bool

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

func (m Metadata) Keys() []string

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

func (m Metadata) Len() int

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

func (m Metadata) Merge(other Metadata) Metadata

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

func (m Metadata) Set(key string, value any) Metadata

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

Log discards the event without recording it.

Always returns nil (success) regardless of event content.

func (*NopAuditLogger) Query

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

func (*NopAuthProvider) Validate

func (p *NopAuthProvider) Validate(_ context.Context, _ string) (*AuthInfo, error)

Validate always returns a valid local user with admin privileges.

The token parameter is ignored - any value (including empty string) results in successful authentication. This is intentional for local single-user deployments.

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

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

func (a *NopRequestAuditor) GetChainLength(_ context.Context, _ string) (int, error)

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

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.

Jump to

Keyboard shortcuts

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