mirror of
https://github.com/mudler/LocalAI.git
synced 2026-06-18 21:58:58 -04:00
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:
committed by
GitHub
parent
c133ca39dc
commit
3fa7b2955c
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user