claw-code/.guardrails/docs/agentmcp/Sentinel Core Kernel.txt

351 lines
11 KiB
Plaintext

/*
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()
}