feat(pii): NER tier engine — privacy-filter.cpp backend + NER-centric PII filter (#10360)

Squashed feat/pii-ner-tier-engine rebased onto master (was 45 commits; see
backup/pii-ner-tier-engine-prerebase). Net change:

- privacy-filter.cpp: standalone GGML engine for the openai-privacy-filter
  PII/NER token classifier, wired as a LocalAI gRPC backend (CPU/CUDA/Vulkan).
  TokenClassify moves off the patched llama.cpp path onto this backend.
- PII filter reworked to be NER-centric (encoder/NER detection tier scanning
  whole conversations as one document), with a recreated bounded restricted-
  regex secret-matching pattern detector tier alongside it (per-model
  pii_detection.builtins / .patterns + core/services/routing/piipattern).
- Detection labelled by source (ner vs pattern); backend trace / confidence /
  debug observability; analyze/redact exposed as a synchronous API.
- Instance-wide default detector policy + per-usecase default-on; request
  filtering extended to completions, embeddings, edits & Ollama.
- React UI: NER-centric PII editor, detector-models table, pattern/builtins
  editor, middleware default-policy UI.
- Gallery: privacy-filter-multilingual token-classify model + NER install
  filter; token_classify known_usecase; batch sized to context for NER models.
  privacy-filter backend registered in the backend gallery (cpu/vulkan/cuda-13
  meta + image entries with a capabilities map) matching its CI matrix jobs,
  and an /import-model auto-detect importer (PrivacyFilterImporter, narrow
  privacy-filter GGUF detection) replacing the prior pref-only registration.

Reconciled against master's independent evolution:

- Dropped master's PIIPatternOverrides feature (global-pattern runtime
  overrides + /api/pii/patterns API + runtime_settings.json persistence). The
  per-model NER + pattern-detector design supersedes it; it was built on the
  global redactor pattern set this branch replaced.
- Reverted the llama.cpp Score carry-patch (0006-server-task-type-score):
  removed the patch and restored master's grpc-server.cpp Score RPC (direct
  llama_decode, slot-loop bypass) and LLAMA_VERSION pin, plus master's
  model_config validation forbidding score + chat/completion/embeddings on
  llama-cpp. token_classify is unaffected (it runs on the privacy-filter
  backend, not llama-cpp).

Assisted-by: Claude:claude-opus-4-8 [Claude Code]

Signed-off-by: Richard Palethorpe <io@richiejp.com>
This commit is contained in:
Richard Palethorpe
2026-06-18 11:45:22 +01:00
committed by GitHub
parent c133ca39dc
commit 3fa7b2955c
134 changed files with 6671 additions and 4223 deletions

View File

@@ -76,24 +76,11 @@ type LocalAIClient interface {
GetUsageStats(ctx context.Context, q UsageStatsQuery) (*UsageStats, error)
// ---- PII filter ----
// ListPIIPatterns returns the active PII pattern set with each
// one's action.
ListPIIPatterns(ctx context.Context) ([]PIIPattern, error)
// GetPIIEvents returns recent redaction events. Implementation
// enforces "admin required" when auth is on.
// enforces "admin required" when auth is on. The regex pattern tools
// were removed — detection policy lives on each detector model's
// pii_detection block, managed via the model-config tools.
GetPIIEvents(ctx context.Context, q PIIEventsQuery) ([]PIIEvent, error)
// TestPIIRedaction dry-runs the redactor against text. No event
// is recorded.
TestPIIRedaction(ctx context.Context, req PIIRedactTestRequest) (*PIIRedactTestResult, error)
// SetPIIPatternAction mutates the named pattern's action and/or
// disabled state in-process. Transient until PersistPIIPatterns is
// called — runtime_settings.json then applies the deltas on the
// next start. Admin-required.
SetPIIPatternAction(ctx context.Context, req PIIPatternActionUpdate) error
// PersistPIIPatterns snapshots the live redactor's per-pattern
// (action, disabled) state into runtime_settings.json. Admin-required.
PersistPIIPatterns(ctx context.Context) error
// ---- Middleware admin ----
// GetMiddlewareStatus returns the aggregated state surfaced on the

View File

@@ -38,25 +38,21 @@ var toolToHTTPRoute = map[string]string{
ToolVRAMEstimate: "POST /api/models/vram-estimate",
ToolGetBranding: "GET /api/branding",
ToolGetUsageStats: "GET /api/usage (or /api/usage/all when all=true)",
ToolListPIIPatterns: "GET /api/pii/patterns",
ToolGetPIIEvents: "GET /api/pii/events",
ToolTestPIIRedaction: "POST /api/pii/test",
ToolGetMiddlewareStatus: "GET /api/middleware/status",
ToolGetRouterDecisions: "GET /api/router/decisions",
// Mutating tools.
ToolInstallModel: "POST /models/apply",
ToolImportModelURI: "POST /models/import-uri",
ToolDeleteModel: "POST /models/delete/:name",
ToolEditModelConfig: "PATCH /api/models/config-json/:name",
ToolReloadModels: "POST /models/reload",
ToolInstallBackend: "POST /backends/apply",
ToolUpgradeBackend: "POST /backends/upgrade/:name",
ToolToggleModelState: "PUT /models/toggle-state/:name/:action",
ToolToggleModelPinned: "PUT /models/toggle-pinned/:name/:action",
ToolSetBranding: "POST /api/settings (instance_name, instance_tagline)",
ToolSetPIIPatternAction: "PUT /api/pii/patterns/:id",
ToolPersistPIIPatterns: "POST /api/pii/patterns/persist",
ToolInstallModel: "POST /models/apply",
ToolImportModelURI: "POST /models/import-uri",
ToolDeleteModel: "POST /models/delete/:name",
ToolEditModelConfig: "PATCH /api/models/config-json/:name",
ToolReloadModels: "POST /models/reload",
ToolInstallBackend: "POST /backends/apply",
ToolUpgradeBackend: "POST /backends/upgrade/:name",
ToolToggleModelState: "PUT /models/toggle-state/:name/:action",
ToolToggleModelPinned: "PUT /models/toggle-pinned/:name/:action",
ToolSetBranding: "POST /api/settings (instance_name, instance_tagline)",
}
// allKnownTools is the union of expectedFullCatalog (defined in

View File

@@ -77,11 +77,11 @@ type Backend struct {
// SystemInfo summarises the LocalAI deployment.
type SystemInfo struct {
Version string `json:"version"`
Distributed bool `json:"distributed"`
BackendsPath string `json:"backends_path,omitempty"`
ModelsPath string `json:"models_path,omitempty"`
LoadedModels []string `json:"loaded_models,omitempty"`
Version string `json:"version"`
Distributed bool `json:"distributed"`
BackendsPath string `json:"backends_path,omitempty"`
ModelsPath string `json:"models_path,omitempty"`
LoadedModels []string `json:"loaded_models,omitempty"`
InstalledBackends []string `json:"installed_backends,omitempty"`
}
@@ -184,19 +184,11 @@ type UsageBucket struct {
// ---- PII / sensitive data tools ----
// PIIPattern is one row in the list_pii_patterns response.
type PIIPattern struct {
ID string `json:"id"`
Description string `json:"description"`
Action string `json:"action"` // mask | block | allow
MaxMatchLength int `json:"max_match_length"`
}
// PIIEventsQuery filters get_pii_events.
type PIIEventsQuery struct {
CorrelationID string `json:"correlation_id,omitempty" jsonschema:"Optional X-Correlation-ID join key (binds events to the request and usage record)."`
UserID string `json:"user_id,omitempty" jsonschema:"Optional user id to scope the query."`
PatternID string `json:"pattern_id,omitempty" jsonschema:"Optional pattern id (e.g. email, ssn)."`
PatternID string `json:"pattern_id,omitempty" jsonschema:"Optional detector group id (e.g. ner:EMAIL)."`
Limit int `json:"limit,omitempty" jsonschema:"Maximum events. Defaults to 100."`
}
@@ -215,38 +207,6 @@ type PIIEvent struct {
CreatedAt string `json:"created_at"`
}
// PIIRedactTestRequest is the input for test_pii_redaction.
type PIIRedactTestRequest struct {
Text string `json:"text" jsonschema:"The candidate text. Will be run through the redactor without recording an event."`
}
// PIIRedactTestResult is the output for test_pii_redaction. spans
// describes where the redactor matched; redacted is the text after
// applying mask actions; blocked / masked flag what was done.
type PIIRedactTestResult struct {
Redacted string `json:"redacted"`
Spans []PIIEventSpan `json:"spans"`
Blocked bool `json:"blocked"`
Masked bool `json:"masked"`
}
type PIIEventSpan struct {
Start int `json:"start"`
End int `json:"end"`
Pattern string `json:"pattern"`
HashPrefix string `json:"hash_prefix"`
}
// PIIPatternActionUpdate is the input for set_pii_pattern_action.
// At least one of Action or Disabled must be set. Mutations are
// transient by default — call persist_pii_patterns to flush them
// to runtime_settings.json so the next start re-applies them.
type PIIPatternActionUpdate struct {
ID string `json:"id" jsonschema:"Pattern id to mutate (e.g. email, ssn, credit_card, api_key_prefix)."`
Action string `json:"action,omitempty" jsonschema:"New action: mask, block, or allow. Optional — omit to leave the action unchanged."`
Disabled *bool `json:"disabled,omitempty" jsonschema:"Set true to skip this pattern entirely; false to re-enable. Optional — omit to leave enabled-state unchanged."`
}
// MiddlewareStatus is the aggregated /api/middleware/status payload —
// the React Middleware page renders this in one go. Routing is a
// placeholder until subsystem 2 lands.
@@ -255,25 +215,25 @@ type MiddlewareStatus struct {
Router MiddlewareRouterStatus `json:"router"`
}
// MiddlewarePIIStatus shows what the redactor is doing right now and
// which models opt in. enabled_globally=false means --disable-pii.
// MiddlewarePIIStatus shows which models opt in to PII redaction and the
// NER detector models they reference. The detection policy itself lives
// on each detector model's pii_detection block.
type MiddlewarePIIStatus struct {
EnabledGlobally bool `json:"enabled_globally"`
Reason string `json:"reason,omitempty"`
DefaultEnabledForBackends []string `json:"default_enabled_for_backends,omitempty"`
Patterns []PIIPattern `json:"patterns"`
Models []MiddlewarePIIModel `json:"models"`
RecentEventCount int `json:"recent_event_count"`
EnabledGlobally bool `json:"enabled_globally"`
Reason string `json:"reason,omitempty"`
DefaultEnabledForBackends []string `json:"default_enabled_for_backends,omitempty"`
Models []MiddlewarePIIModel `json:"models"`
RecentEventCount int `json:"recent_event_count"`
}
// MiddlewarePIIModel is one model row in the per-model PII table.
type MiddlewarePIIModel struct {
Name string `json:"name"`
Backend string `json:"backend"`
Enabled bool `json:"enabled"`
Explicit bool `json:"explicit"` // Did YAML set Enabled, or did the backend prefix decide?
DefaultForBackend bool `json:"default_for_backend"` // Backend matches the auto-on rule (proxy-*).
Overrides map[string]string `json:"overrides,omitempty"`
Name string `json:"name"`
Backend string `json:"backend"`
Enabled bool `json:"enabled"`
Explicit bool `json:"explicit"` // Did YAML set Enabled, or did the backend prefix decide?
DefaultForBackend bool `json:"default_for_backend"` // Backend matches the auto-on rule (proxy-*).
Detectors []string `json:"detectors,omitempty"` // NER detector model names this config references.
}
// MiddlewareRouterStatus is the placeholder shape the Routing tab

View File

@@ -45,10 +45,7 @@ type fakeClient struct {
getBranding func() (*Branding, error)
setBranding func(SetBrandingRequest) (*Branding, error)
getUsageStats func(UsageStatsQuery) (*UsageStats, error)
listPIIPatterns func() ([]PIIPattern, error)
getPIIEvents func(PIIEventsQuery) ([]PIIEvent, error)
testPIIRedaction func(PIIRedactTestRequest) (*PIIRedactTestResult, error)
setPIIPatternAction func(PIIPatternActionUpdate) error
getMiddlewareStatus func() (*MiddlewareStatus, error)
getRouterDecisions func(RouterDecisionsQuery) ([]RouterDecision, error)
}
@@ -253,14 +250,6 @@ func (f *fakeClient) GetUsageStats(_ context.Context, q UsageStatsQuery) (*Usage
}, nil
}
func (f *fakeClient) ListPIIPatterns(_ context.Context) ([]PIIPattern, error) {
f.record("ListPIIPatterns", nil)
if f.listPIIPatterns != nil {
return f.listPIIPatterns()
}
return []PIIPattern{}, nil
}
func (f *fakeClient) GetPIIEvents(_ context.Context, q PIIEventsQuery) ([]PIIEvent, error) {
f.record("GetPIIEvents", q)
if f.getPIIEvents != nil {
@@ -269,27 +258,6 @@ func (f *fakeClient) GetPIIEvents(_ context.Context, q PIIEventsQuery) ([]PIIEve
return []PIIEvent{}, nil
}
func (f *fakeClient) TestPIIRedaction(_ context.Context, req PIIRedactTestRequest) (*PIIRedactTestResult, error) {
f.record("TestPIIRedaction", req)
if f.testPIIRedaction != nil {
return f.testPIIRedaction(req)
}
return &PIIRedactTestResult{Redacted: req.Text}, nil
}
func (f *fakeClient) SetPIIPatternAction(_ context.Context, req PIIPatternActionUpdate) error {
f.record("SetPIIPatternAction", req)
if f.setPIIPatternAction != nil {
return f.setPIIPatternAction(req)
}
return nil
}
func (f *fakeClient) PersistPIIPatterns(_ context.Context) error {
f.record("PersistPIIPatterns", nil)
return nil
}
func (f *fakeClient) GetRouterDecisions(_ context.Context, q RouterDecisionsQuery) ([]RouterDecision, error) {
f.record("GetRouterDecisions", q)
if f.getRouterDecisions != nil {
@@ -306,10 +274,8 @@ func (f *fakeClient) GetMiddlewareStatus(_ context.Context) (*MiddlewareStatus,
return &MiddlewareStatus{
PII: MiddlewarePIIStatus{
EnabledGlobally: true,
Patterns: []PIIPattern{},
Models: []MiddlewarePIIModel{},
},
Router: MiddlewareRouterStatus{Configured: false, Models: []string{}},
}, nil
}

View File

@@ -582,16 +582,6 @@ func (c *Client) GetUsageStats(ctx context.Context, q localaitools.UsageStatsQue
// ---- PII filter ----
func (c *Client) ListPIIPatterns(ctx context.Context) ([]localaitools.PIIPattern, error) {
var raw struct {
Patterns []localaitools.PIIPattern `json:"patterns"`
}
if err := c.do(ctx, http.MethodGet, routePIIPatterns, nil, &raw); err != nil {
return nil, err
}
return raw.Patterns, nil
}
func (c *Client) GetPIIEvents(ctx context.Context, q localaitools.PIIEventsQuery) ([]localaitools.PIIEvent, error) {
qs := url.Values{}
if q.CorrelationID != "" {
@@ -624,35 +614,6 @@ func (c *Client) GetPIIEvents(ctx context.Context, q localaitools.PIIEventsQuery
return raw.Events, nil
}
func (c *Client) TestPIIRedaction(ctx context.Context, req localaitools.PIIRedactTestRequest) (*localaitools.PIIRedactTestResult, error) {
var out localaitools.PIIRedactTestResult
if err := c.do(ctx, http.MethodPost, routePIITest, map[string]string{"text": req.Text}, &out); err != nil {
return nil, err
}
return &out, nil
}
func (c *Client) SetPIIPatternAction(ctx context.Context, req localaitools.PIIPatternActionUpdate) error {
if req.ID == "" {
return fmt.Errorf("pattern id is required")
}
body := map[string]any{}
if req.Action != "" {
body["action"] = req.Action
}
if req.Disabled != nil {
body["disabled"] = *req.Disabled
}
if len(body) == 0 {
return fmt.Errorf("must specify action and/or disabled")
}
return c.do(ctx, http.MethodPut, routePIIPatternByID(req.ID), body, nil)
}
func (c *Client) PersistPIIPatterns(ctx context.Context) error {
return c.do(ctx, http.MethodPost, routePIIPatternsPersist, nil, nil)
}
func (c *Client) GetMiddlewareStatus(ctx context.Context) (*localaitools.MiddlewareStatus, error) {
var out localaitools.MiddlewareStatus
if err := c.do(ctx, http.MethodGet, routeMiddleware, nil, &out); err != nil {

View File

@@ -11,33 +11,26 @@ import (
// registrations in core/http/routes/localai.go — the Tool↔REST drift detector
// in coverage_test.go documents the mapping.
const (
routeWelcome = "/"
routeModelsApply = "/models/apply"
routeModelsAvail = "/models/available"
routeModelsGall = "/models/galleries"
routeModelsImport = "/models/import-uri"
routeModelsReload = "/models/reload"
routeBackends = "/backends"
routeBackendsKnown = "/backends/known"
routeBackendsApply = "/backends/apply"
routeNodes = "/api/nodes"
routeVRAMEstimate = "/api/models/vram-estimate"
routeBranding = "/api/branding"
routeSettings = "/api/settings"
routeUsage = "/api/usage"
routeUsageAll = "/api/usage/all"
routePIIPatterns = "/api/pii/patterns"
routePIIPatternsPersist = "/api/pii/patterns/persist"
routePIIEvents = "/api/pii/events"
routePIITest = "/api/pii/test"
routeMiddleware = "/api/middleware/status"
routeRouterDecisions = "/api/router/decisions"
routeWelcome = "/"
routeModelsApply = "/models/apply"
routeModelsAvail = "/models/available"
routeModelsGall = "/models/galleries"
routeModelsImport = "/models/import-uri"
routeModelsReload = "/models/reload"
routeBackends = "/backends"
routeBackendsKnown = "/backends/known"
routeBackendsApply = "/backends/apply"
routeNodes = "/api/nodes"
routeVRAMEstimate = "/api/models/vram-estimate"
routeBranding = "/api/branding"
routeSettings = "/api/settings"
routeUsage = "/api/usage"
routeUsageAll = "/api/usage/all"
routePIIEvents = "/api/pii/events"
routeMiddleware = "/api/middleware/status"
routeRouterDecisions = "/api/router/decisions"
)
func routePIIPatternByID(id string) string {
return "/api/pii/patterns/" + url.PathEscape(id)
}
func routeJobStatus(jobID string) string {
return "/models/jobs/" + url.PathEscape(jobID)
}

View File

@@ -14,10 +14,10 @@ import (
"github.com/mudler/LocalAI/core/config"
"github.com/mudler/LocalAI/core/gallery"
"github.com/mudler/LocalAI/core/gallery/importers"
"github.com/mudler/LocalAI/core/http/auth"
"github.com/mudler/LocalAI/core/schema"
"github.com/mudler/LocalAI/core/services/galleryop"
"github.com/mudler/LocalAI/core/services/modeladmin"
"github.com/mudler/LocalAI/core/http/auth"
"github.com/mudler/LocalAI/core/services/routing/billing"
"github.com/mudler/LocalAI/core/services/routing/pii"
"github.com/mudler/LocalAI/core/services/routing/router"
@@ -619,23 +619,6 @@ func (c *Client) GetUsageStats(ctx context.Context, q localaitools.UsageStatsQue
// ---- PII filter ----
func (c *Client) ListPIIPatterns(_ context.Context) ([]localaitools.PIIPattern, error) {
if c.PIIRedactor == nil {
return nil, errors.New("PII filter is disabled")
}
patterns := c.PIIRedactor.Patterns()
out := make([]localaitools.PIIPattern, 0, len(patterns))
for _, p := range patterns {
out = append(out, localaitools.PIIPattern{
ID: p.ID,
Description: p.Description,
Action: string(p.Action),
MaxMatchLength: p.MaxMatchLength,
})
}
return out, nil
}
func (c *Client) GetPIIEvents(ctx context.Context, q localaitools.PIIEventsQuery) ([]localaitools.PIIEvent, error) {
if c.PIIEvents == nil {
return nil, errors.New("PII filter is disabled")
@@ -668,77 +651,6 @@ func (c *Client) GetPIIEvents(ctx context.Context, q localaitools.PIIEventsQuery
return out, nil
}
func (c *Client) SetPIIPatternAction(_ context.Context, req localaitools.PIIPatternActionUpdate) error {
if c.PIIRedactor == nil {
return errors.New("PII filter is disabled")
}
if req.ID == "" {
return errors.New("pattern id is required")
}
if req.Action == "" && req.Disabled == nil {
return errors.New("must specify action and/or disabled")
}
if req.Action != "" {
if err := c.PIIRedactor.SetAction(req.ID, pii.Action(req.Action)); err != nil {
return err
}
}
if req.Disabled != nil {
if err := c.PIIRedactor.SetDisabled(req.ID, *req.Disabled); err != nil {
return err
}
}
return nil
}
// PersistPIIPatterns snapshots the current redactor state into
// runtime_settings.json. Mirrors POST /api/pii/patterns/persist.
func (c *Client) PersistPIIPatterns(_ context.Context) error {
if c.PIIRedactor == nil {
return errors.New("PII filter is disabled")
}
if c.AppConfig == nil {
return errors.New("app config not available")
}
existing, err := c.AppConfig.ReadPersistedSettings()
if err != nil {
return fmt.Errorf("read settings: %w", err)
}
defaults, err := pii.LoadConfig(c.AppConfig.PIIConfigPath)
if err != nil {
return fmt.Errorf("reload defaults: %w", err)
}
defaultByID := make(map[string]pii.Pattern, len(defaults))
for _, d := range defaults {
defaultByID[d.ID] = d
}
overrides := map[string]config.PIIPatternRuntimeOverride{}
for _, p := range c.PIIRedactor.Patterns() {
d, known := defaultByID[p.ID]
ov := config.PIIPatternRuntimeOverride{}
changed := false
if !known || p.Action != d.Action {
action := string(p.Action)
ov.Action = &action
changed = true
}
if !known || p.Disabled != d.Disabled {
disabled := p.Disabled
ov.Disabled = &disabled
changed = true
}
if changed {
overrides[p.ID] = ov
}
}
existing.PIIPatternOverrides = &overrides
if err := c.AppConfig.WritePersistedSettings(existing); err != nil {
return fmt.Errorf("write settings: %w", err)
}
c.AppConfig.PIIPatternOverrides = overrides
return nil
}
func (c *Client) GetRouterDecisions(ctx context.Context, q localaitools.RouterDecisionsQuery) ([]localaitools.RouterDecision, error) {
if c.RouterDecisions == nil {
return []localaitools.RouterDecision{}, nil
@@ -779,23 +691,10 @@ func (c *Client) GetMiddlewareStatus(ctx context.Context) (*localaitools.Middlew
Note: "Intelligent routing is not yet implemented.",
}
piiSection := localaitools.MiddlewarePIIStatus{
EnabledGlobally: c.PIIRedactor != nil,
Patterns: []localaitools.PIIPattern{},
EnabledGlobally: c.PIIEvents != nil,
Models: []localaitools.MiddlewarePIIModel{},
}
if c.PIIRedactor == nil {
piiSection.Reason = "--disable-pii"
return &localaitools.MiddlewareStatus{PII: piiSection, Router: router}, nil
}
piiSection.DefaultEnabledForBackends = []string{"cloud-proxy"}
for _, p := range c.PIIRedactor.Patterns() {
piiSection.Patterns = append(piiSection.Patterns, localaitools.PIIPattern{
ID: p.ID,
Description: p.Description,
Action: string(p.Action),
MaxMatchLength: p.MaxMatchLength,
})
}
if c.ConfigLoader != nil {
for _, cfg := range c.ConfigLoader.GetAllModelsConfigs() {
cfg := cfg
@@ -805,7 +704,7 @@ func (c *Client) GetMiddlewareStatus(ctx context.Context) (*localaitools.Middlew
Enabled: cfg.PIIIsEnabled(),
Explicit: cfg.PII.Enabled != nil,
DefaultForBackend: cfg.Backend == "cloud-proxy",
Overrides: cfg.PIIPatternOverrides(),
Detectors: cfg.PIIDetectors(),
})
}
}
@@ -817,27 +716,6 @@ func (c *Client) GetMiddlewareStatus(ctx context.Context) (*localaitools.Middlew
return &localaitools.MiddlewareStatus{PII: piiSection, Router: router}, nil
}
func (c *Client) TestPIIRedaction(_ context.Context, req localaitools.PIIRedactTestRequest) (*localaitools.PIIRedactTestResult, error) {
if c.PIIRedactor == nil {
return nil, errors.New("PII filter is disabled")
}
res := c.PIIRedactor.Redact(req.Text)
out := &localaitools.PIIRedactTestResult{
Redacted: res.Redacted,
Blocked: res.Blocked,
Masked: res.Masked,
}
for _, s := range res.Spans {
out.Spans = append(out.Spans, localaitools.PIIEventSpan{
Start: s.Start,
End: s.End,
Pattern: s.Pattern,
HashPrefix: s.HashPrefix,
})
}
return out, nil
}
func capabilityFlagsOf(m *config.ModelConfig) []string {
var out []string
for label, flag := range config.GetAllModelConfigUsecases() {

View File

@@ -91,13 +91,9 @@ var expectedFullCatalog = sortedStrings(
ToolListInstalledModels,
ToolListKnownBackends,
ToolListNodes,
ToolListPIIPatterns,
ToolPersistPIIPatterns,
ToolReloadModels,
ToolSetBranding,
ToolSetPIIPatternAction,
ToolSystemInfo,
ToolTestPIIRedaction,
ToolToggleModelPinned,
ToolToggleModelState,
ToolUpgradeBackend,
@@ -119,9 +115,7 @@ var expectedReadOnlyCatalog = sortedStrings(
ToolListInstalledModels,
ToolListKnownBackends,
ToolListNodes,
ToolListPIIPatterns,
ToolSystemInfo,
ToolTestPIIRedaction,
ToolVRAMEstimate,
)

View File

@@ -20,9 +20,7 @@ const (
ToolVRAMEstimate = "vram_estimate"
ToolGetBranding = "get_branding"
ToolGetUsageStats = "get_usage_stats"
ToolListPIIPatterns = "list_pii_patterns"
ToolGetPIIEvents = "get_pii_events"
ToolTestPIIRedaction = "test_pii_redaction"
ToolGetMiddlewareStatus = "get_middleware_status"
ToolGetRouterDecisions = "get_router_decisions"
@@ -38,8 +36,6 @@ const (
ToolToggleModelState = "toggle_model_state"
ToolToggleModelPinned = "toggle_model_pinned"
ToolSetBranding = "set_branding"
ToolSetPIIPatternAction = "set_pii_pattern_action"
ToolPersistPIIPatterns = "persist_pii_patterns"
)
// DefaultServerName is the MCP Implementation.Name surfaced when

View File

@@ -7,21 +7,21 @@ import (
)
// registerMiddlewareTools wires the routing-module admin surface for the
// MCP server. The two tools mirror what the React /app/middleware page
// exposes:
// MCP server, mirroring what the React /app/middleware page exposes:
//
// - get_middleware_status: read-only aggregator. The agent can ask
// "what's filtering my requests?" and get back the active PII
// pattern set, the per-model resolved enabled/override state, and
// a placeholder for routing.
// - set_pii_pattern_action: mutating. Mutations are TRANSIENT — they
// live until process restart, when patterns reload from the YAML
// defaults. The skill prompt should warn the user about that
// before applying lasting changes.
func registerMiddlewareTools(s *mcp.Server, client LocalAIClient, opts Options) {
// "what's filtering my requests?" and get back the per-model PII
// enabled state + the detector models each references, recent event
// count, plus the active router models and their classifier configs.
// - get_router_decisions: read-only routing-decision log.
//
// PII detection policy lives on each detector model's pii_detection
// block, edited via the model-config tools — there is no global pattern
// set to mutate here anymore.
func registerMiddlewareTools(s *mcp.Server, client LocalAIClient, _ Options) {
mcp.AddTool(s, &mcp.Tool{
Name: ToolGetMiddlewareStatus,
Description: "Aggregated routing-module status: PII pattern catalogue with current actions, per-model resolved PII state and overrides, recent event count, plus the active router models and their classifier configs. Read-only.",
Description: "Aggregated routing-module status: per-model resolved PII state and the NER detector models each one references, recent event count, plus the active router models and their classifier configs. Read-only.",
}, func(ctx context.Context, _ *mcp.CallToolRequest, _ struct{}) (*mcp.CallToolResult, any, error) {
status, err := client.GetMiddlewareStatus(ctx)
if err != nil {
@@ -40,39 +40,4 @@ func registerMiddlewareTools(s *mcp.Server, client LocalAIClient, opts Options)
}
return jsonResult(decisions), nil, nil
})
if opts.DisableMutating {
return
}
mcp.AddTool(s, &mcp.Tool{
Name: ToolSetPIIPatternAction,
Description: "Change a PII pattern's action (mask|block|allow) and/or disabled state in-process. TRANSIENT: the mutation is lost on restart unless followed by persist_pii_patterns. Admin-required.",
}, func(ctx context.Context, _ *mcp.CallToolRequest, args PIIPatternActionUpdate) (*mcp.CallToolResult, any, error) {
if args.ID == "" {
return errorResultf("id is required"), nil, nil
}
if args.Action == "" && args.Disabled == nil {
return errorResultf("at least one of action (mask, block, allow) or disabled must be set"), nil, nil
}
if err := client.SetPIIPatternAction(ctx, args); err != nil {
return errorResult(err), nil, nil
}
return jsonResult(map[string]any{
"id": args.ID,
"action": args.Action,
"disabled": args.Disabled,
"persisted": false,
}), nil, nil
})
mcp.AddTool(s, &mcp.Tool{
Name: ToolPersistPIIPatterns,
Description: "Snapshot the live PII redactor's per-pattern (action, disabled) state into runtime_settings.json so it re-applies on the next process start. Pairs with set_pii_pattern_action — that one is in-process; this one persists. Admin-required.",
}, func(ctx context.Context, _ *mcp.CallToolRequest, _ struct{}) (*mcp.CallToolResult, any, error) {
if err := client.PersistPIIPatterns(ctx); err != nil {
return errorResult(err), nil, nil
}
return jsonResult(map[string]any{"persisted": true}), nil, nil
})
}

View File

@@ -7,20 +7,13 @@ import (
)
func registerPIITools(s *mcp.Server, client LocalAIClient, _ Options) {
mcp.AddTool(s, &mcp.Tool{
Name: ToolListPIIPatterns,
Description: "List the active PII regex pattern set. Each entry shows the pattern id, description, and current action (mask, block, allow). Read-only.",
}, func(ctx context.Context, _ *mcp.CallToolRequest, _ struct{}) (*mcp.CallToolResult, any, error) {
patterns, err := client.ListPIIPatterns(ctx)
if err != nil {
return errorResult(err), nil, nil
}
return jsonResult(patterns), nil, nil
})
// The regex pattern tools (list/test/set/persist) were removed with
// the regex tier. Detection policy now lives on each detector model's
// pii_detection block (managed via the model config tools/UI), so the
// only PII tool is the read-only audit-event view.
mcp.AddTool(s, &mcp.Tool{
Name: ToolGetPIIEvents,
Description: "Recent PII redaction events. Filter by correlation_id (joins to a usage record), user_id, or pattern_id. Events never carry the matched value — only an 8-char sha256 prefix so admins can dedupe recurring leaks.",
Description: "Recent PII redaction events. Filter by correlation_id (joins to a usage record), user_id, or pattern_id (e.g. ner:EMAIL). Events never carry the matched value — only an 8-char sha256 prefix so admins can dedupe recurring leaks.",
}, func(ctx context.Context, _ *mcp.CallToolRequest, args PIIEventsQuery) (*mcp.CallToolResult, any, error) {
events, err := client.GetPIIEvents(ctx, args)
if err != nil {
@@ -28,18 +21,4 @@ func registerPIITools(s *mcp.Server, client LocalAIClient, _ Options) {
}
return jsonResult(events), nil, nil
})
mcp.AddTool(s, &mcp.Tool{
Name: ToolTestPIIRedaction,
Description: "Dry-run the PII redactor against text without recording a real event. Useful for tuning patterns: paste a candidate string and see whether it would be masked, blocked, or routed locally.",
}, func(ctx context.Context, _ *mcp.CallToolRequest, args PIIRedactTestRequest) (*mcp.CallToolResult, any, error) {
if args.Text == "" {
return errorResultf("text is required"), nil, nil
}
res, err := client.TestPIIRedaction(ctx, args)
if err != nil {
return errorResult(err), nil, nil
}
return jsonResult(res), nil, nil
})
}