Add async third-party vetting

Queue vetting on third_parties with PENDING, PROCESSING,
COMPLETED, and FAILED states. Expose enqueue and status through
GraphQL, MCP, CLI, and n8n, validate vet requests, tune the
worker via config, and poll the detail page while vetting runs.

Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
This commit is contained in:
Sacha Al Himdani
2026-05-27 13:34:49 +02:00
parent 1a71d15bc5
commit 6e7c96732f
59 changed files with 3251 additions and 520 deletions

View File

@@ -17,30 +17,31 @@ package probod
import "go.probo.inc/probo/pkg/probodconfig"
type (
FullConfig = probodconfig.FullConfig
Config = probodconfig.Config
UnitConfig = probodconfig.UnitConfig
MetricsConfig = probodconfig.MetricsConfig
TracingConfig = probodconfig.TracingConfig
ESignConfig = probodconfig.ESignConfig
TrustCenterConfig = probodconfig.TrustCenterConfig
APIConfig = probodconfig.APIConfig
CorsConfig = probodconfig.CorsConfig
ProxyProtocolConfig = probodconfig.ProxyProtocolConfig
AuthConfig = probodconfig.AuthConfig
OAuth2ServerConfig = probodconfig.OAuth2ServerConfig
OAuth2SigningKeyConfig = probodconfig.OAuth2SigningKeyConfig
CookieConfig = probodconfig.CookieConfig
PasswordConfig = probodconfig.PasswordConfig
AWSConfig = probodconfig.AWSConfig
ConnectorConfig = probodconfig.ConnectorConfig
ConnectorConfigOAuth2 = probodconfig.ConnectorConfigOAuth2
CustomDomainsConfig = probodconfig.CustomDomainsConfig
ACMEConfig = probodconfig.ACMEConfig
LLMProviderConfig = probodconfig.LLMProviderConfig
LLMAgentConfig = probodconfig.LLMAgentConfig
EvidenceDescriberConfig = probodconfig.EvidenceDescriberConfig
AgentsConfig = probodconfig.AgentsConfig
FullConfig = probodconfig.FullConfig
Config = probodconfig.Config
UnitConfig = probodconfig.UnitConfig
MetricsConfig = probodconfig.MetricsConfig
TracingConfig = probodconfig.TracingConfig
ESignConfig = probodconfig.ESignConfig
TrustCenterConfig = probodconfig.TrustCenterConfig
APIConfig = probodconfig.APIConfig
CorsConfig = probodconfig.CorsConfig
ProxyProtocolConfig = probodconfig.ProxyProtocolConfig
AuthConfig = probodconfig.AuthConfig
OAuth2ServerConfig = probodconfig.OAuth2ServerConfig
OAuth2SigningKeyConfig = probodconfig.OAuth2SigningKeyConfig
CookieConfig = probodconfig.CookieConfig
PasswordConfig = probodconfig.PasswordConfig
AWSConfig = probodconfig.AWSConfig
ConnectorConfig = probodconfig.ConnectorConfig
ConnectorConfigOAuth2 = probodconfig.ConnectorConfigOAuth2
CustomDomainsConfig = probodconfig.CustomDomainsConfig
ACMEConfig = probodconfig.ACMEConfig
LLMProviderConfig = probodconfig.LLMProviderConfig
LLMAgentConfig = probodconfig.LLMAgentConfig
EvidenceDescriberConfig = probodconfig.EvidenceDescriberConfig
ThirdPartyVettingWorkerConfig = probodconfig.ThirdPartyVettingWorkerConfig
AgentsConfig = probodconfig.AgentsConfig
TrackerMappingWorkerConfig = probodconfig.TrackerMappingWorkerConfig
CommonPatternEnrichmentWorkerConfig = probodconfig.CommonPatternEnrichmentWorkerConfig

View File

@@ -177,6 +177,11 @@ func New() *Implm {
StaleAfter: 300,
MaxConcurrency: 10,
},
ThirdPartyVetting: ThirdPartyVettingWorkerConfig{
Interval: 10,
StaleAfter: 1500,
MaxConcurrency: 1,
},
},
}
}
@@ -309,7 +314,7 @@ func (impl *Implm) Run(
return err
}
thirdPartyAssessor, err := impl.buildThirdPartyAssessor(l, tp, r)
thirdPartyVetter, err := impl.buildThirdPartyVetter(l, tp, r)
if err != nil {
return err
}
@@ -522,7 +527,6 @@ func (impl *Implm) Run(
esignService,
defaultConnectorRegistry,
time.Duration(impl.cfg.Auth.InvitationConfirmationTokenValidity)*time.Second,
thirdPartyAssessor,
)
if err != nil {
return fmt.Errorf("cannot create probo service: %w", err)
@@ -552,7 +556,7 @@ func (impl *Implm) Run(
l.Named("access-review"),
)
thirdPartyService := thirdparty.NewService(pgClient, fileService)
thirdPartyService := thirdparty.NewService(pgClient, fileService, thirdPartyVetter)
riskManagementService := riskmanagement.NewService(pgClient)
serverHandler, err := server.NewServer(
@@ -818,6 +822,26 @@ func (impl *Implm) Run(
},
)
vettingWorker := thirdparty.NewVettingWorker(
pgClient,
thirdPartyVetter,
l.Named("vetting-worker"),
thirdparty.VettingWorkerConfig{
StaleAfter: time.Duration(impl.cfg.ThirdPartyVetting.StaleAfter) * time.Second,
},
worker.WithInterval(time.Duration(impl.cfg.ThirdPartyVetting.Interval)*time.Second),
worker.WithMaxConcurrency(impl.cfg.ThirdPartyVetting.MaxConcurrency),
)
vettingWorkerCtx, stopVettingWorker := context.WithCancel(context.Background())
wg.Go(
func() {
if err := vettingWorker.Run(vettingWorkerCtx); err != nil {
cancel(fmt.Errorf("vetting worker crashed: %w", err))
}
},
)
trustCenterServerCtx, stopTrustCenterServer := context.WithCancel(context.Background())
defer stopTrustCenterServer()
@@ -849,6 +873,7 @@ func (impl *Implm) Run(
stopTrackerMappingWorker()
stopCommonPatternEnrichmentWorker()
stopMailingListWorker()
stopVettingWorker()
stopEvidenceDescriptionWorker()
stopDocumentPDFWorker()
stopExportJobExporter()

View File

@@ -18,26 +18,19 @@ import (
"github.com/prometheus/client_golang/prometheus"
"go.gearno.de/kit/log"
"go.opentelemetry.io/otel/trace"
"go.probo.inc/probo/pkg/probo"
"go.probo.inc/probo/pkg/thirdparty"
"go.probo.inc/probo/pkg/vetting"
)
// buildThirdPartyAssessor wires the thirdParty assessment agent. It is an opt-in
// feature: deployments that do not set `llm.third-party-assessor.provider` get a
// DisabledThirdPartyAssessor that reports the feature as unavailable. The
// third-party-assessor does not inherit the default provider because its
// pipeline (LLM + browser + search) is expensive and should not be enabled
// implicitly.
func (impl *Implm) buildThirdPartyAssessor(
// buildThirdPartyVetter wires the third-party vetting agent. Unset
// third-party-vetter fields inherit from the default agent config
// (AGENT_DEFAULT_*), same as evidence-describer and probo.
func (impl *Implm) buildThirdPartyVetter(
l *log.Logger,
tp trace.TracerProvider,
r prometheus.Registerer,
) (probo.ThirdPartyAssessor, error) {
if impl.cfg.Agents.ThirdPartyAssessor.Provider == "" {
return probo.DisabledThirdPartyAssessor{}, nil
}
agentCfg, llmClient, err := impl.resolveAgentClient("third-party-assessor", impl.cfg.Agents.ThirdPartyAssessor, l, tp, r)
) (thirdparty.Vetter, error) {
agentCfg, llmClient, err := impl.resolveAgentClient("third-party-vetter", impl.cfg.Agents.ThirdPartyVetter, l, tp, r)
if err != nil {
return nil, err
}
@@ -53,6 +46,6 @@ func (impl *Implm) buildThirdPartyAssessor(
MaxTokens: maxTokens,
ChromeAddr: impl.cfg.ChromeDPAddr,
FirecrawlAPIKey: impl.cfg.Agents.Tools.FirecrawlAPIKey,
Logger: l.Named("third-party-assessor"),
Logger: l.Named("third-party-vetter"),
}), nil
}