---
title: Forge Extension
description: Mount Sentinel into a Forge application as a first-class extension with automatic route registration and migrations.
---

Sentinel ships a ready-made Forge extension in the `extension` package. It wires the engine, HTTP API, and lifecycle management into Forge's extension system.

## Installation

```go
import "github.com/xraph/sentinel/extension"
```

## Registering the extension

```go
package main

import (
    "github.com/xraph/forge"
    "github.com/xraph/sentinel/extension"
)

func main() {
    app := forge.New()

    sentinelExt := extension.New(
        extension.WithGroveDatabase(""), // resolve default grove.DB from DI
    )

    app.RegisterExtension(sentinelExt)
    app.Run()
}
```

Or with an explicit store:

```go
sentinelExt := extension.New(
    extension.WithStore(pgstore.New(db)),
)
```

## What the extension does

| Lifecycle event | Behaviour |
|----------------|-----------|
| `Register` | Creates the engine from the provided options |
| `Start` | Runs `store.Migrate` (unless disabled) |
| `RegisterRoutes` | Mounts all Sentinel HTTP endpoints under the base path |
| `Stop` | Calls `engine.Stop` -- emits `OnShutdown` to all plugins |

## Extension options

| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `WithStore(s)` | `store.Store` | -- | Composite store (auto-resolved from grove if not set) |
| `WithExtension(x)` | `plugin.Extension` | -- | Lifecycle hook plugin (repeatable) |
| `WithEngineOption(opt)` | `engine.Option` | -- | Pass engine option directly |
| `WithConfig(cfg)` | `Config` | defaults | Full config struct |
| `WithDisableRoutes()` | -- | `false` | Skip HTTP route registration |
| `WithDisableMigrate()` | -- | `false` | Skip migrations on Start |
| `WithBasePath(path)` | `string` | `""` | URL prefix for all sentinel routes |
| `WithGroveDatabase(name)` | `string` | `""` | Name of the grove.DB to resolve from DI |
| `WithRequireConfig(b)` | `bool` | `false` | Require config in YAML files |

## File-based configuration (YAML)

When running in a Forge application, the Sentinel extension automatically loads configuration from YAML config files. The extension looks for config under the following keys (in order):

1. `extensions.sentinel` -- standard Forge extension config namespace
2. `sentinel` -- top-level shorthand

### Example YAML config

```yaml
# forge.yaml
extensions:
  sentinel:
    disable_routes: false
    disable_migrate: false
    base_path: "/sentinel"
    default_model: "smart"
    temperature: 0
    pass_threshold: 0.7
    concurrency: 4
    shutdown_timeout: "30s"
    grove_database: ""
```

Or using the top-level shorthand:

```yaml
# forge.yaml
sentinel:
  default_model: "gpt-4o"
  pass_threshold: 0.8
  concurrency: 8
```

### Config fields

| YAML Key | Type | Default | Description |
|----------|------|---------|-------------|
| `disable_routes` | `bool` | `false` | Skip HTTP route registration |
| `disable_migrate` | `bool` | `false` | Skip migrations on Start |
| `base_path` | `string` | `""` | URL prefix for all routes |
| `default_model` | `string` | `"smart"` | LLM model identifier |
| `temperature` | `float64` | `0` | LLM sampling temperature |
| `pass_threshold` | `float64` | `0.7` | Minimum score to pass a test case |
| `concurrency` | `int` | `4` | Parallel evaluation workers |
| `shutdown_timeout` | `duration` | `"30s"` | Max graceful shutdown wait time |
| `grove_database` | `string` | `""` | Named grove.DB to resolve from DI |

### Merge behaviour

File-based configuration is merged with programmatic options. Programmatic boolean flags (`DisableRoutes`, `DisableMigrate`) always win when set to `true`. For other fields, YAML values take precedence, then programmatic values, then defaults.

### Requiring configuration

If your deployment requires YAML config to be present, use `WithRequireConfig`:

```go
ext := extension.New(
    extension.WithRequireConfig(true), // error if no YAML config found
)
```

## Accessing the engine from other extensions

After `Register` is called by Forge, `sentinelExt.Engine()` returns the fully initialised engine:

```go
eng := sentinelExt.Engine()
// use eng.CreateSuite, eng.ListRuns, etc. from another extension
```

## Tenant middleware

In a Forge app, tenant scope is typically set by middleware. Implement a Forge middleware that calls `sentinel.WithTenant` and `sentinel.WithApp`:

```go
func tenantMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        tenantID := r.Header.Get("X-Tenant-ID")
        appID := r.Header.Get("X-App-ID")
        ctx := sentinel.WithTenant(r.Context(), tenantID)
        ctx = sentinel.WithApp(ctx, appID)
        next.ServeHTTP(w, r.WithContext(ctx))
    })
}

router.Use(tenantMiddleware)
```

## Grove database integration

When your Forge app uses the [Grove extension](https://github.com/xraph/grove) to manage database connections, Sentinel can automatically resolve a `grove.DB` from the DI container and construct the correct store backend based on the driver type.

### Using the default grove database

If the Grove extension registers a single database (or a default in multi-DB mode), use `WithGroveDatabase` with an empty name:

```go
ext := extension.New(
    extension.WithGroveDatabase(""),
)
```

### Using a named grove database

In multi-database setups, reference a specific database by name:

```go
ext := extension.New(
    extension.WithGroveDatabase("sentinel"),
)
```

This resolves the grove.DB named `"sentinel"` from the DI container and auto-constructs the matching store. The driver type is detected automatically -- you do not need to import individual store packages.

### Store resolution order

The extension resolves its store in this order:

1. **Explicit store** -- if `WithStore(s)` was called, it is used directly and grove is ignored.
2. **Grove database** -- if `WithGroveDatabase(name)` was called, the named or default `grove.DB` is resolved from DI.
3. **In-memory fallback** -- if neither is configured, an in-memory store is used.

## Adding metrics

Register the observability extension alongside the Sentinel engine extension:

```go
import "github.com/xraph/sentinel/observability"

metricsExt := observability.NewMetricsExtensionWithFactory(fapp.Metrics())

sentinelExt := extension.New(
    extension.WithStore(store),
    extension.WithExtension(metricsExt),
)
```
