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

@@ -206,6 +206,12 @@ func (b *Builder) Build() (*probodconfig.FullConfig, error) {
Temperature: b.getEnvFloatPtr("AGENT_EVIDENCE_DESCRIBER_TEMPERATURE"),
MaxTokens: b.getEnvIntPtr("AGENT_EVIDENCE_DESCRIBER_MAX_TOKENS"),
},
ThirdPartyVetter: probodconfig.LLMAgentConfig{
Provider: b.getEnvOrDefault("AGENT_THIRD_PARTY_VETTER_PROVIDER", ""),
ModelName: b.getEnvOrDefault("AGENT_THIRD_PARTY_VETTER_MODEL_NAME", ""),
Temperature: b.getEnvFloatPtr("AGENT_THIRD_PARTY_VETTER_TEMPERATURE"),
MaxTokens: b.getEnvIntPtr("AGENT_THIRD_PARTY_VETTER_MAX_TOKENS"),
},
TrackerMapping: probodconfig.LLMAgentConfig{
Provider: b.getEnvOrDefault("AGENT_TRACKER_MAPPING_PROVIDER", ""),
ModelName: b.getEnvOrDefault("AGENT_TRACKER_MAPPING_MODEL_NAME", ""),
@@ -246,6 +252,11 @@ func (b *Builder) Build() (*probodconfig.FullConfig, error) {
StaleAfter: b.getEnvIntOrDefault("EVIDENCE_DESCRIBER_STALE_AFTER", 300),
MaxConcurrency: b.getEnvIntOrDefault("EVIDENCE_DESCRIBER_MAX_CONCURRENCY", 10),
},
ThirdPartyVetting: probodconfig.ThirdPartyVettingWorkerConfig{
Interval: b.getEnvIntOrDefault("THIRD_PARTY_VETTING_INTERVAL", 10),
StaleAfter: b.getEnvIntOrDefault("THIRD_PARTY_VETTING_STALE_AFTER", 1500),
MaxConcurrency: b.getEnvIntOrDefault("THIRD_PARTY_VETTING_MAX_CONCURRENCY", 1),
},
TrackerMappingWorker: probodconfig.TrackerMappingWorkerConfig{
Interval: b.getEnvIntOrDefault("TRACKER_MAPPING_INTERVAL", 10),
MaxConcurrency: b.getEnvIntOrDefault("TRACKER_MAPPING_MAX_CONCURRENCY", 3),

View File

@@ -215,6 +215,10 @@ func TestBuilder_Build_Defaults(t *testing.T) {
assert.Empty(t, cfg.Probod.Agents.EvidenceDescriber.ModelName)
assert.Nil(t, cfg.Probod.Agents.EvidenceDescriber.Temperature)
assert.Nil(t, cfg.Probod.Agents.EvidenceDescriber.MaxTokens)
assert.Empty(t, cfg.Probod.Agents.ThirdPartyVetter.Provider)
assert.Empty(t, cfg.Probod.Agents.ThirdPartyVetter.ModelName)
assert.Nil(t, cfg.Probod.Agents.ThirdPartyVetter.Temperature)
assert.Nil(t, cfg.Probod.Agents.ThirdPartyVetter.MaxTokens)
assert.Empty(t, cfg.Probod.Agents.TrackerMapping.Provider)
assert.Empty(t, cfg.Probod.Agents.TrackerMapping.ModelName)
assert.Nil(t, cfg.Probod.Agents.TrackerMapping.Temperature)
@@ -231,6 +235,9 @@ func TestBuilder_Build_Defaults(t *testing.T) {
assert.Equal(t, 600, cfg.Probod.CommonPatternEnrichmentWorker.StaleAfter)
assert.Equal(t, 45, cfg.Probod.CommonPatternEnrichmentWorker.AgentTimeout)
assert.Equal(t, 10, cfg.Probod.CommonPatternEnrichmentWorker.AgentMaxTurns)
assert.Equal(t, 10, cfg.Probod.ThirdPartyVetting.Interval)
assert.Equal(t, 1500, cfg.Probod.ThirdPartyVetting.StaleAfter)
assert.Equal(t, 1, cfg.Probod.ThirdPartyVetting.MaxConcurrency)
// Custom domains config
assert.Equal(t, 3600, cfg.Probod.CustomDomains.RenewalInterval)
@@ -320,6 +327,11 @@ func TestBuilder_Build_CustomValues(t *testing.T) {
env["AGENT_EVIDENCE_DESCRIBER_MODEL_NAME"] = "claude-sonnet-4-20250514"
env["AGENT_EVIDENCE_DESCRIBER_TEMPERATURE"] = "0.2"
env["AGENT_EVIDENCE_DESCRIBER_MAX_TOKENS"] = "4096"
// Agents — third-party-vetter override
env["AGENT_THIRD_PARTY_VETTER_PROVIDER"] = "openai"
env["AGENT_THIRD_PARTY_VETTER_MODEL_NAME"] = "gpt-4o"
env["AGENT_THIRD_PARTY_VETTER_TEMPERATURE"] = "0.3"
env["AGENT_THIRD_PARTY_VETTER_MAX_TOKENS"] = "8192"
// Agents — tracker-mapping override
env["AGENT_TRACKER_MAPPING_PROVIDER"] = "openai"
env["AGENT_TRACKER_MAPPING_MODEL_NAME"] = "gpt-4o-mini"
@@ -336,6 +348,9 @@ func TestBuilder_Build_CustomValues(t *testing.T) {
env["COMMON_PATTERN_ENRICHMENT_STALE_AFTER"] = "900"
env["COMMON_PATTERN_ENRICHMENT_AGENT_TIMEOUT"] = "50"
env["COMMON_PATTERN_ENRICHMENT_AGENT_MAX_TURNS"] = "5"
env["THIRD_PARTY_VETTING_INTERVAL"] = "15"
env["THIRD_PARTY_VETTING_STALE_AFTER"] = "1800"
env["THIRD_PARTY_VETTING_MAX_CONCURRENCY"] = "2"
// Custom domains
env["CUSTOM_DOMAINS_RESOLVER_ADDR"] = "1.1.1.1:53"
env["ACME_ACCOUNT_KEY"] = "-----BEGIN EC PRIVATE KEY-----\ntest\n-----END EC PRIVATE KEY-----"
@@ -422,6 +437,11 @@ func TestBuilder_Build_CustomValues(t *testing.T) {
assert.Equal(t, "claude-sonnet-4-20250514", cfg.Probod.Agents.EvidenceDescriber.ModelName)
assert.Equal(t, new(0.2), cfg.Probod.Agents.EvidenceDescriber.Temperature)
assert.Equal(t, new(4096), cfg.Probod.Agents.EvidenceDescriber.MaxTokens)
// Agents — third-party-vetter overrides
assert.Equal(t, "openai", cfg.Probod.Agents.ThirdPartyVetter.Provider)
assert.Equal(t, "gpt-4o", cfg.Probod.Agents.ThirdPartyVetter.ModelName)
assert.Equal(t, new(0.3), cfg.Probod.Agents.ThirdPartyVetter.Temperature)
assert.Equal(t, new(8192), cfg.Probod.Agents.ThirdPartyVetter.MaxTokens)
// Agents — tracker-mapping overrides
assert.Equal(t, "openai", cfg.Probod.Agents.TrackerMapping.Provider)
assert.Equal(t, "gpt-4o-mini", cfg.Probod.Agents.TrackerMapping.ModelName)
@@ -438,6 +458,9 @@ func TestBuilder_Build_CustomValues(t *testing.T) {
assert.Equal(t, 900, cfg.Probod.CommonPatternEnrichmentWorker.StaleAfter)
assert.Equal(t, 50, cfg.Probod.CommonPatternEnrichmentWorker.AgentTimeout)
assert.Equal(t, 5, cfg.Probod.CommonPatternEnrichmentWorker.AgentMaxTurns)
assert.Equal(t, 15, cfg.Probod.ThirdPartyVetting.Interval)
assert.Equal(t, 1800, cfg.Probod.ThirdPartyVetting.StaleAfter)
assert.Equal(t, 2, cfg.Probod.ThirdPartyVetting.MaxConcurrency)
// Custom domains
assert.Equal(t, "1.1.1.1:53", cfg.Probod.CustomDomains.ResolverAddr)
assert.Equal(t, "-----BEGIN EC PRIVATE KEY-----\ntest\n-----END EC PRIVATE KEY-----", cfg.Probod.CustomDomains.ACME.AccountKey)