A token without admin.directory.customer.readonly makes
Customers.Get return 403 forever. Treat that as terminal so
the source-name worker keeps the generic name and stops
retrying.
Signed-off-by: Bryan Frimin <bryan@probo.com>
Unbounded LoadAll* loaders materialised an entire result set in one
query with no ceiling. A table that is small in development can grow
without bound in production, so these loaders were a latent memory
and query-time hazard.
Remove the LoadAll* methods from pkg/coredata and walk the cursor-
paginated LoadBy* siblings instead through a shared page.LoadAll
helper. The helper advances a MaxCursorSize forward cursor until the
result set is exhausted and concatenates the pages. It caps a single
call at MaxLoadAllPages (20) batches of 500 rows and errors past that
rather than materialising an unbounded set, so a runaway caller fails
loudly instead of exhausting memory.
Callers that genuinely need every row now express that explicitly,
and the coredata load-naming rule and docs are updated to discourage
new unbounded loaders.
Signed-off-by: Sacha Al Himdani <sacha@probo.com>
The release guide was refactored from a single contrib/claude/release.md
into a contrib/claude/release/ directory with a README index and per-track
guides, but the AGENTS.md link was never updated and pointed at a missing
file. Repoint it to release/README.md. CLAUDE.md is a symlink to AGENTS.md,
so it inherits the fix.
Signed-off-by: Sacha Al Himdani <sacha@probo.com>
The MCP Profile schema declares additional_email_addresses as a
required, non-nullable array, but NewProfile passed the mail.Addrs
value through unchanged. A profile with no extra emails has a nil
slice, which marshals to JSON null and fails tool output validation
with "type: null, want array".
Default the nil slice to an empty mail.Addrs so the marshalled output
always honors the strict array schema.
Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
Long role strings in the access review table broke row layout when
drivers joined many roles into one comma-separated value. Expose
roles as a string array in GraphQL by splitting the stored role at
the API layer, and render the first three roles as badges with a
"+X more" popover for the rest.
Closes ENG-459.
Signed-off-by: Ludovic Vielle <ludovic@probo.com>
Employee document signatures recorded an acknowledgment with no
cryptographic proof, unlike document approvals which already create
and accept an electronic signature on every decision.
Mirror the approval flow on the sign path: generate the signed
document PDF, create-and-accept an esign record, and persist its id
on the document_version_signatures row through a new
electronic_signature_id column. Capture the signer IP and user agent
in the resolver, and re-check the published/archived preconditions
inside the transaction so the seal cannot race document state.
Make the consent wording a single backend source of truth shared by
the text that is sealed and the text shown in the UI. Define
DocumentSignatureConsentText and DocumentApprovalConsentText in the
probo service package and the NDA copy in the trust service, each
owned by the flow that uses it, and stop esign from appending the
generic clause to caller-provided consent text so approvals no
longer seal a duplicated sentence.
Expose the resolved consent text through GraphQL on
EmployeeDocumentVersion and DocumentVersionApprovalDecision, and have
the signing, approval, and NDA pages render it from the API instead
of hard-coded strings, mirroring how the NDA page already worked.
Align the wording with the actual interaction: the buttons read
"Review and sign" and "Review and approve", the clauses reference
those actions, and the inaccurate "typing my full name" phrasing is
removed everywhere.
Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
Denials were invisible in the audit trail and evaluator explainability
(policy_id, reason) was discarded before reaching logs. Emit a structured
authz decision line on every evaluation in evaluateMultiInTx — allow,
deny, no_match, and assumption errors — using the existing authorizer
logger with opaque IDs only.
Add decision_log.go with DecisionRecord and logDecision. Surface
PolicyID and Reason on EvaluationResult for logging. Audit log
behavior is unchanged (allow-only). Document the convention in
authorization.md.
Signed-off-by: Ludovic Vielle <ludovic@probo.com>
The campaign source node test queried entries before starting the
campaign, and the lifecycle test used the old accessEntry field name
instead of accessReviewEntry.
Signed-off-by: Bryan Frimin <bryan@probo.com>
Expose campaign sources as first-class nodes, paginate fetch attempts
instead of denormalized status fields, and bind entries to their
campaign snapshot. Update GraphQL, MCP, CLI, console, and e2e coverage
to match.
Signed-off-by: Bryan Frimin <bryan@probo.com>
Source category was never surfaced in the product and added noise to
snapshots and APIs. Remove the enum, columns, and service fields so
campaign sources track only the identity fields reviewers need.
Signed-off-by: Bryan Frimin <bryan@probo.com>
Fresh installs already had csv_data on campaign sources from the
initial migration; add a follow-up migration to backfill existing
rows from the live source snapshot.
Signed-off-by: Bryan Frimin <bryan@probo.com>
Bad API keys and expired OAuth tokens showed Connected because
probes ran only for OAuth2 and many providers had no ProbeURL.
Add a registry ProbeConnection dispatcher with static, dynamic,
and custom probes so all 41 providers are checked on demand.
Signed-off-by: Bryan Frimin <bryan@probo.com>
Cursor's Admin API exposes two independent removal signals: the isRemoved
boolean and a role value of "removed". They are not always consistent —
a member can carry role "removed" while isRemoved is still false, a known
gap documented on the Cursor community forum.
Previously Active was derived from isRemoved alone, so a member with
role "removed" but isRemoved=false was incorrectly reported as active.
Now either signal is sufficient to mark the account inactive.
Add a cassette entry and test case covering the inconsistent state
(role "removed", isRemoved false) to prevent regression.
Signed-off-by: Bryan Frimin <bryan@probo.com>
Decouple each campaign from the live access-review sources it was started
with by introducing a per-campaign source snapshot table
(access_review_campaign_sources). The snapshot captures the source name,
category, and connector at start time, so a review remains coherent even
after the underlying source is edited or deleted. Fetch tracking becomes
an append-only log (access_review_campaign_source_fetch_attempts) that
preserves every attempt with its own status and error rather than
overwriting a single row.
Rename the shared access-review tables and enums to use a consistent
access_review_ prefix throughout:
access_entries → access_review_entries
access_sources → access_review_sources
access_source_category → access_review_source_category
access_entry_* → access_review_entry_*
The same rename propagates to every coredata type, service, GraphQL
schema, MCP specification, CLI command, frontend component, and e2e test.
The accessreview package gains dedicated actions.go and policies.go files
for its own IAM policy set, mirroring the agentrun package pattern.
Signed-off-by: Bryan Frimin <bryan@probo.com>
The enrichment agent now reads full cookie-database and vendor pages
through the browser. The facts it gathers (setter, purpose, data
stored) are not protectable, but a source page's written description
is original expression. Instruct the agent to state the facts in its
own words and never copy a sentence verbatim, so stored descriptions
stay clear of third-party prose.
Signed-off-by: Émile Ré <emile@probo.com>
The tracker-mapping and common-pattern enrichment agents only had
web search, which returns title/url/snippet, so they could never
open a cookie-database or cookie-policy page to read which vendor
actually sets a tracker. This mis-attributed setters whose snippet
is misleading (e.g. _li_* read as LinkedIn rather than LiveIntent).
Wire the read-only headless-browser toolset into both agents, gated
on a configured Chrome endpoint, mirroring the common-third-party
enrichment worker: agent construction moves into the run path so each
run can carry a per-run browser that is closed when the run returns.
The prompts now direct the agent to open a promising result and read
the named setter from the full page text. Both agents stay unchanged
when no Chrome endpoint is configured.
Signed-off-by: Émile Ré <emile@probo.com>
Brand assets served at /api/files/v1/static had no cache headers.
Introduce brand.Assets to own the embedded filesystem, content-hash
ETags, and HTTP serving. Responses now carry Cache-Control and ETag
so clients can cache and revalidate; stable email URLs stay
revalidatable (max-age=3600, no immutable).
Replace hardcoded Default*Path constants with StaticPathPrefix,
logical filename constants, and StaticPath(). NewAssets validates
required assets at startup so a rename fails fast instead of 404ing
in sent emails. The files handler keeps routing and 404 rendering;
ServeAssets sets cache headers and serves the file.
Signed-off-by: Ludovic Vielle <ludovic@probo.com>
The preference panel scroll lock relied on `overflow: hidden` on the
document element, which JS-driven smooth-scroll libraries such as Lenis
(used by Framer, e.g. getbluejay.ai) bypass entirely since they
intercept wheel/touch events and scroll programmatically.
Pin the body with `position: fixed` (saving and restoring the scroll
position) so the viewport has no scroll distance left to animate, even
for libraries that capture wheel events on the window before us. Keep
capture-phase wheel/touch cancellation as defense-in-depth for sites
that scroll an inner container, while still letting the panel's own
list scroll.
Signed-off-by: Émile Ré <emile@probo.com>
Address PR review feedback: rename the shared string-helper package
from strutil to stringsx to avoid the discouraged util suffix and the
collision with the standard strings package, updating all import paths
and call sites.
Replace the manual wg.Add/wg.Done bookkeeping in the enrichment worker
with wg.Go, which is less error-prone.
Signed-off-by: Émile Ré <emile@probo.com>
The alphanumeric-normalisation helper was duplicated verbatim in the
common-third-party owned-domains resolver and the cookiebanner tracker
mapping worker. Hoist it into a new dependency-free strutil package so
both call sites share one implementation and one test.
Signed-off-by: Émile Ré <emile@probo.com>
The enrichment_attempts column dropped its default after backfill, so
inserts must supply it explicitly. The e2e seed helper omitted the
column and tripped the NOT NULL constraint, failing
TestThirdParty_ImportFromCommon and TestCommonThirdParties_QueryWithLogo.
Set it to 0 in the seed insert.
Signed-off-by: Émile Ré <emile@probo.com>
The common-third-party enrichment pipeline ran Agent B (compliance
docs), Agent C (owned domains), and the deterministic logo step
sequentially even though, once Agent A resolves the website, the
three depend only on that website and not on each other. Fan them
out across goroutines under a WaitGroup so wall time is the slowest
of the three rather than their sum. Each step builds its own per-run
browser and writes only into its own locals; the shared LLM, HTTP,
and FileManager clients are safe for concurrent use and the database
is untouched until persist. Results merge in a fixed order so
runErrors and log output stay deterministic.
Also replace the single-sentence confidence guidance in the three
agent prompts with an explicit, calibrated rubric tied to evidence
strength, and remind the model that a downstream threshold gates
persistence so it should neither inflate nor deflate its estimates.
Signed-off-by: Émile Ré <emile@probo.com>
Reject oversized logo responses instead of silently truncating them,
which could persist corrupt image bytes as a valid logo.
Tighten ownership substring matching with a length-ratio guard so a
short label root no longer attributes unrelated domains to a vendor.
Render the worker confidence threshold when set to zero by testing
against nil, so an explicit "accept all" value is not dropped by Helm's
falsy-numeric truthiness.
Sanitize and bound per-agent error text before persisting it to the
enrichment metadata column to avoid leaking unbounded internal detail.
Signed-off-by: Émile Ré <emile@probo.com>
Columns must not carry DEFAULT clauses. The DEFAULT only backfills
existing rows when the non-nullable column is added, so drop it in the
same migration; inserts already supply enrichment_attempts explicitly.
Signed-off-by: Émile Ré <emile@probo.com>
Add `proboctl common-third-party reenrich` to re-arm the async
enrichment worker for selected catalog rows, and `stats` to summarize
the catalog by enrichment state and last run status. Rows are selected
verbatim via --id/--slug or across the catalog via
--category/--keyword/--state/--status, gated by --dry-run and --yes.
Extend `list` with --state/--status filters and STATE/STATUS columns,
and `show` with enrichment state, attempts, last run status, error,
per-field provenance, and discovered domains.
Back these with CommonThirdPartyFilter state/status/IDs filters plus
CommonThirdParties.LoadAllIDs and RequestEnrichmentByIDs. The latter
stamps enrichment_requested_at and resets the attempt counter while
preserving the existing payload, so the worker merge keeps curated and
human-edited provenance.
Also simplify exactLabelMatch to use slices.Contains.
Signed-off-by: Émile Ré <emile@probo.com>
When the common-third-party enrichment worker discovers owned domains
for a vendor, tracker patterns that were detected and left unmatched
before those domains were known had nothing to re-trigger them.
Add RequestMappingForUnmappedByInitiatorDomains, a global re-arm that
stamps mapping_requested_at on still-unmapped patterns whose detected
trackers share one of the new domains, and call it from the enrichment
worker's persist step for newly-inserted domains only. The mapping
worker then re-resolves them through its existing domain-overlap path.
Targeting is limited to patterns with no resolved vendor (no org third
party and an absent or unlinked catalog row), so a pattern already
attributed to this or any other vendor is never disturbed.
Signed-off-by: Émile Ré <emile@probo.com>
Add a domain-discovery step to the enrichment pipeline so the catalog's
domain set, previously written only by the curated seed, grows
automatically. A focused agent enumerates the registrable domains a
vendor owns and operates - marketing, product and sub-brand, app, API,
and CDN/asset domains - from links seen while browsing and from web
search, anchored on the website resolved earlier in the run.
A deterministic ownership gate reduces the candidates to eTLD+1 and
keeps only those that clear a strict confidence floor and match the
vendor by domain label. Shared tracker-delivery and CDN infrastructure
is dropped unless the vendor itself is that provider, in which case its
own brand-matching domain passes a stricter exact-label check. The
survivors are upserted into common_third_party_domains in the run's
final transaction and recorded in the enrichment payload, feeding the
tracker-mapping domain step and disambiguation.
Signed-off-by: Émile Ré <emile@probo.com>
Give the company-profile agent (Agent A) the read-only browser toolset
and build it per-run, so it can read footer, imprint, about, and legal
pages and follow a product domain to the corporate one to resolve the
legal name and headquarters address rather than failing cold.
Make the website the hard precondition: when Agent A cannot resolve a
canonical website, skip the compliance-docs agent and logo step instead
of running them blind, which previously produced inconsistent
cross-domain document URLs. Fall back to the catalog display name for
the legal name when nothing better is found, recorded with a distinct
provenance status so a later real find overwrites it.
Rewrite both enrichment prompts in the project's role/task/instructions
XML style, add a domain-consistency rule for document URLs and a
tool-budget directive, and document the prompt style as a rule and guide.
Fix the find_links_matching browser tool, which double-encoded its
pattern and made JSON.parse fail on every keyword, starving any agent
that used it until it hit the turn cap. Salvage output when an agent
exhausts its turn budget while still exploring with a pending structured
output by forcing one final synthesis turn instead of failing outright.
Signed-off-by: Émile Ré <emile@probo.com>