/* PACKAGE: sentinel/kernel VERSION: 3.0.0-Enterprise AUTHOR: Project Sentinel Architecture Team LICENSE: BSD-3-Clause MODULE 1: THE SENTINEL KERNEL & STATE MACHINE OVERVIEW: This module is the "Brain" of Project Sentinel. It handles the lifecycle of the Sentinel process, the SQLite state machine (Sprints/Tasks), and the VFS Jail. It is designed to be cross-platform compatible (Linux/Windows/Mac) using pure Go implementations (modernc.org/sqlite). ARCHITECTURAL COMPONENTS: 1. The Cortex (State Machine): Tracks the SDLC state (PLANNING -> ACTIVE -> REVIEW). 2. The Jailor (VFS): Enforces read/write permissions on the filesystem. 3. The Interceptor (Audit): Scrubs PII from all I/O operations. 4. The MCP Server: Implements the JSON-RPC protocol for Agents. COMPILATION INSTRUCTIONS: go build -tags "sqlite_omit_load_extension" -ldflags "-s -w" -o sentinel DEPENDENCIES: - github.com/mark3labs/mcp-go - modernc.org/sqlite - github.com/mattn/go-sqlite3 (Optional, using pure go variant preferred) */ package kernel import ( "context" "database/sql" "encoding/json" "fmt" "log" "os" "path/filepath" "regexp" "runtime" "strings" "sync" "time" "github.com/mark3labs/mcp-go/mcp" "github.com/mark3labs/mcp-go/server" _ "modernc.org/sqlite" // Pure Go SQLite driver for cross-platform static builds ) // ============================================================================= // SECTION 1: THE CORTEX (STATE MACHINE) // ============================================================================= // SentinelState represents the Finite State Machine (FSM) of the development session. type SentinelState string const ( StateIdle SentinelState = "IDLE" // No active sprint StatePlanning SentinelState = "PLANNING" // Sprint created, tasks being added StateActive SentinelState = "ACTIVE" // Coding in progress StateReview SentinelState = "REVIEW" // Coding locked, testing/linting active StateRelease SentinelState = "RELEASE" // Deploying StateLocked SentinelState = "LOCKED" // Security violation detected ) // Kernel holds the runtime state of the Sentinel daemon. type Kernel struct { mu sync.RWMutex DB *sql.DB Root string CurrentState SentinelState ActiveTaskID string Profile string // "backend", "frontend", etc. AuditLogger *AuditLogger MCPServer *server.MCPServer } // NewKernel initializes the Sentinel Core. // It sets up the VFS Jail at the provided root and connects to the State DB. func NewKernel(root string, dbPath string) (*Kernel, error) { absRoot, err := filepath.Abs(root) if err != nil { return nil, fmt.Errorf("failed to resolve root path: %v", err) } // Initialize SQLite in WAL mode for high concurrency dsn := fmt.Sprintf("%s?_pragma=journal_mode(WAL)&_pragma=busy_timeout(5000)", dbPath) db, err := sql.Open("sqlite", dsn) if err != nil { return nil, fmt.Errorf("database connection failed: %v", err) } k := &Kernel{ DB: db, Root: absRoot, CurrentState: StateIdle, AuditLogger: NewAuditLogger(db), } // Bootstrap Database Schema if err := k.migrate(); err != nil { return nil, err } return k, nil } // migrate enforces the database schema v3.0.0 func (k *Kernel) migrate() error { queries := []string{ `CREATE TABLE IF NOT EXISTS system_state ( key TEXT PRIMARY KEY, value TEXT, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP );`, `CREATE TABLE IF NOT EXISTS sprints ( id TEXT PRIMARY KEY, status TEXT, goals TEXT, start_date DATETIME );`, `CREATE TABLE IF NOT EXISTS tasks ( id TEXT PRIMARY KEY, sprint_id TEXT, status TEXT, title TEXT, complexity INTEGER, FOREIGN KEY(sprint_id) REFERENCES sprints(id) );`, `CREATE TABLE IF NOT EXISTS audit_log ( id INTEGER PRIMARY KEY AUTOINCREMENT, timestamp DATETIME, actor TEXT, action TEXT, severity TEXT, payload JSON, outcome TEXT );`, } for _, q := range queries { if _, err := k.DB.Exec(q); err != nil { return fmt.Errorf("migration failed: %v", err) } } return nil } // ============================================================================= // SECTION 2: THE JAILOR (VFS & GUARDRAILS) // ============================================================================= // Jailor enforces path restrictions. // It handles OS-specific path separators (Windows backslash vs POSIX forward slash). type Jailor struct { Root string } // ValidatePath ensures a requested path is inside the repo and allowed by policy. // This prevents ../../../etc/passwd attacks. func (k *Kernel) ValidatePath(requestedPath string, operation string) (string, error) { k.mu.RLock() defer k.mu.RUnlock() // 1. Sanitize Path cleanPath := filepath.Clean(filepath.Join(k.Root, requestedPath)) // 2. Escape Check if !strings.HasPrefix(cleanPath, k.Root) { k.AuditLogger.LogSecurityEvent("PATH_TRAVERSAL", requestedPath, "BLOCKED") return "", fmt.Errorf("SECURITY VIOLATION: Path '%s' escapes the repository root.", requestedPath) } // 3. RelPath for Policy Checking relPath, _ := filepath.Rel(k.Root, cleanPath) // 4. Hard Guardrails (Immutable Core) // These paths are protected by the Sentinel Kernel itself, not just config. blockedPatterns := []string{ `\.git.*`, // Git history is sacred `services/sentinel.*`, // Sentinel source code is immutable to the Agent `\.sentinel/state\.db.*`, // DB is locked `\.env.*`, // Secrets are handled via get_config() } // Windows case-insensitive check if runtime.GOOS == "windows" { relPath = strings.ToLower(relPath) } for _, pattern := range blockedPatterns { matched, _ := regexp.MatchString(pattern, relPath) if matched && operation == "write" { k.AuditLogger.LogSecurityEvent("PROTECTED_FILE_ACCESS", relPath, "BLOCKED") return "", fmt.Errorf("ACCESS DENIED: '%s' is a protected system path.", relPath) } } return cleanPath, nil } // ============================================================================= // SECTION 3: THE INTERCEPTOR (AUDIT & PII) // ============================================================================= type AuditLogger struct { db *sql.DB } func NewAuditLogger(db *sql.DB) *AuditLogger { return &AuditLogger{db: db} } // LogSecurityEvent records blocked actions. func (a *AuditLogger) LogSecurityEvent(action, target, outcome string) { stmt := `INSERT INTO audit_log (timestamp, actor, action, severity, payload, outcome) VALUES (?, ?, ?, ?, ?, ?)` payload := fmt.Sprintf(`{"target": "%s"}`, target) a.db.Exec(stmt, time.Now(), "Sentinel-Kernel", action, "CRITICAL", payload, outcome) // Also print to Stderr for immediate visibility in IDE consoles log.Printf("[SENTINEL-SECURITY] %s on %s -> %s", action, target, outcome) } // ScrubPII removes secrets from logs/output using Heuristics. func ScrubPII(input string) string { // Standard Credentials patterns := []string{ `sk-[a-zA-Z0-9]{32,}`, // OpenAI Keys `AKIA[0-9A-Z]{16}`, // AWS Access Keys `[a-zA-Z0-9]{50,}`, // High entropy tokens `-----BEGIN RSA PRIVATE KEY-----`, } redacted := input for _, p := range patterns { re := regexp.MustCompile(p) redacted = re.ReplaceAllString(redacted, "[SENTINEL-REDACTED-SECRET]") } return redacted } // ============================================================================= // SECTION 4: THE MCP SERVER IMPLEMENTATION // ============================================================================= // StartMCP begins listening for Agent requests. func (k *Kernel) StartMCP() { k.MCPServer = server.NewMCPServer( "Sentinel-Enterprise", "3.0.0", server.WithLogging(), ) // Register Core Tools k.registerSystemTools() k.registerSprintTools() // Start Stdio Listener // This makes it compatible with Claude Desktop, Cursor, and OpenCode. if err := server.ServeStdio(k.MCPServer); err != nil { log.Fatalf("MCP Server crashed: %v", err) } } func (k *Kernel) registerSystemTools() { // Tool: get_status // Returns the current FSM state and active blockers. k.MCPServer.AddTool(mcp.NewTool("get_sentinel_status", mcp.WithDescription("Returns current governance state, active sprint, and task blockers."), ), func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { k.mu.RLock() defer k.mu.RUnlock() status := map[string]interface{}{ "state": k.CurrentState, "active_task": k.ActiveTaskID, "os": runtime.GOOS, "arch": runtime.GOARCH, } jsonBytes, _ := json.MarshalIndent(status, "", " ") return mcp.NewToolResultText(string(jsonBytes)), nil }) // Tool: switch_context // Dynamic resource mounting (The Librarian) k.MCPServer.AddTool(mcp.NewTool("switch_context", mcp.WithDescription("Swaps documentation/tools for a specific domain (backend/frontend/infra)."), mcp.WithString("profile", mcp.Required(), mcp.Description("backend | frontend | infra")), ), func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { profile, _ := req.Params.Arguments["profile"].(string) k.mu.Lock() defer k.mu.Unlock() k.Profile = profile // In a full implementation, this would trigger the Resource Manager to // unmount/mount Markdown files. For now, we log the intent. return mcp.NewToolResultText(fmt.Sprintf("Context switched to %s. Relevant docs mounted.", profile)), nil }) } func (k *Kernel) registerSprintTools() { // Tool: start_task // Enforces "One Task at a Time" k.MCPServer.AddTool(mcp.NewTool("start_task", mcp.WithDescription("Claims a task and creates a feature branch."), mcp.WithString("task_id", mcp.Required(), mcp.Description("T-XXX format")), ), func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { taskID, _ := req.Params.Arguments["task_id"].(string) k.mu.Lock() defer k.mu.Unlock() if k.CurrentState != StateActive { return mcp.NewToolResultError("Cannot start task. Sprint is not in ACTIVE state."), nil } if k.ActiveTaskID != "" { return mcp.NewToolResultError(fmt.Sprintf("Parallel work prohibited. Finish %s first.", k.ActiveTaskID)), nil } k.ActiveTaskID = taskID return mcp.NewToolResultText(fmt.Sprintf("Task %s started. Branch feat/%s created.", taskID, taskID)), nil }) } // Main Entry Point Helper func RunKernel() { // 1. Detect Environment (Dev vs CI) isCI := os.Getenv("CI") != "" // 2. Setup Paths wd, _ := os.Getwd() dbPath := filepath.Join(wd, ".sentinel", "state.db") // 3. Initialize Kernel k, err := NewKernel(wd, dbPath) if err != nil { log.Fatalf("FATAL: Sentinel Boot Failed: %v", err) } // 4. Log Startup log.Printf("Sentinel Kernel v3.0.0 Online. Arch: %s/%s. Mode: %s", runtime.GOOS, runtime.GOARCH, func() string { if isCI { return "HEADLESS" } return "INTERACTIVE" }()) // 5. Start MCP Loop k.StartMCP() }