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

@@ -30,7 +30,7 @@ import (
const (
// DefaultMaxTokens is the fallback max-tokens budget used when the
// third-party-assessor agent config does not specify a value. Sized to
// third-party-vetter agent config does not specify a value. Sized to
// leave headroom above the orchestrator's thinking budget on
// Anthropic models.
DefaultMaxTokens = 16384
@@ -173,7 +173,7 @@ func NewAssessor(cfg Config) *Assessor {
return &Assessor{cfg: cfg}
}
func (a *Assessor) Assess(ctx context.Context, websiteURL string, procedure string, reporter agent.ProgressReporter) (*Result, error) {
func (a *Assessor) Assess(ctx context.Context, websiteURL string, procedure string, reporter agent.ProgressReporter, extraTools []agent.Tool) (*Result, error) {
u, err := url.Parse(websiteURL)
if err != nil {
return nil, fmt.Errorf("cannot parse website URL %q: %w", websiteURL, err)
@@ -193,15 +193,12 @@ func (a *Assessor) Assess(ctx context.Context, websiteURL string, procedure stri
ctx, cancel := context.WithTimeout(context.WithoutCancel(ctx), AssessmentTimeout)
defer cancel()
thirdPartyBrowser := browser.NewBrowser(ctx, a.cfg.ChromeAddr)
defer thirdPartyBrowser.Close()
thirdPartyBrowser.SetAllowedDomain(u.Hostname())
// Create an unrestricted browser for web search agents that need to
// follow links to external sites (news, reviews, etc.).
researchBrowser := browser.NewBrowser(ctx, a.cfg.ChromeAddr)
defer researchBrowser.Close()
// One shared remote Chrome allocator for all sub-agents. Sub-agents
// that need external links (subprocessor hosts, research) share it
// with vendor-site crawlers. Navigation is still gated by public-IP
// checks; we do not pin an allowed domain so external follows work.
webBrowser := browser.NewBrowser(ctx, a.cfg.ChromeAddr)
defer webBrowser.Close()
orchestrator, err := newOrchestratorAgent(
a.cfg.Client,
@@ -209,16 +206,16 @@ func (a *Assessor) Assess(ctx context.Context, websiteURL string, procedure stri
a.cfg.MaxTokens,
procedure,
a.cfg.Logger,
thirdPartyBrowser,
researchBrowser,
webBrowser,
a.cfg.FirecrawlAPIKey,
reporter,
extraTools,
)
if err != nil {
return nil, fmt.Errorf("cannot create orchestrator agent: %w", err)
}
result, err := orchestrator.Run(
orchestratorResult, err := orchestrator.Run(
ctx,
[]llm.Message{
{
@@ -231,7 +228,10 @@ func (a *Assessor) Assess(ctx context.Context, websiteURL string, procedure stri
return nil, fmt.Errorf("cannot assess thirdParty: %w", err)
}
document := result.FinalMessage().Text()
document := orchestratorResult.FinalMessage().Text()
// Extraction is LLM-only; release Chrome before it runs.
webBrowser.Close()
reportProgress(ctx, reporter, "extract_third_party_info", agent.ProgressEventStepStarted)
@@ -241,6 +241,13 @@ func (a *Assessor) Assess(ctx context.Context, websiteURL string, procedure stri
return nil, fmt.Errorf("cannot extract thirdParty info: %w", err)
}
toolSubprocessors := subprocessorsFromOrchestratorMessages(orchestratorResult.Messages)
info.Subprocessors = mergeSubprocessors(toolSubprocessors, info.Subprocessors)
if info.SubprocessorsListURL == "" {
info.SubprocessorsListURL = subprocessorListURLFromOrchestratorMessages(orchestratorResult.Messages)
}
reportProgress(ctx, reporter, "extract_third_party_info", agent.ProgressEventStepCompleted)
return &Result{
@@ -335,7 +342,12 @@ func thirdPartyInfoOutputType() (*agent.OutputType, error) {
return nil, fmt.Errorf("cannot marshal decorated thirdParty info schema: %w", err)
}
outputType.Schema = decorated
strict, err := enforceStrictJSONSchema(decorated)
if err != nil {
return nil, fmt.Errorf("cannot enforce strict thirdParty info schema: %w", err)
}
outputType.Schema = strict
return outputType, nil
}