Prefix the unknown-tool error with the "cannot" convention and drop
the duplicate wrap around executeSingleTool: that helper already wraps
its generic error path, so the outer wrap produced messages shaped like
"cannot execute tool X: cannot execute tool X: ...".
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
MaxTurns is the only agent bound compared against a counter that is
serialised in the checkpoint (Turns). When config drifts between save
and restore -- typically because a deploy changed WithMaxTurns or a
different build of the agent is registered by name -- cp.Turns can
exceed agent.maxTurns on the resumed run, which previously surfaced
as a warning log and then a MaxTurnsExceededError on the first
iteration of the resumed coreLoop.
Capture MaxTurns in the new AgentConfig on every save, and on
restore clone the registry-resolved agent with WithMaxTurns applied
from the snapshot. The override flows through the outer Restore path
and through both inner-agent resolution sites in
restoreNestedSuspended and restoreAwaitingApproval, so nested
runs get the same treatment. Other loop bounds
(maxEmptyOutputRetries, maxToolDepth) reset per turn / per tool
depth and stay intentionally live so deploys can tune them without
invalidating in-flight checkpoints. Live references (tools, hooks,
LLM client, approval callbacks, guardrails) are not snapshotted for
the same reason.
With the snapshot in place, the "restored agent run has already
reached max turns" warning at the top of continueFromMessages is
structurally unreachable -- the live agent's bound is now the same
value cp.Turns was bounded by at save time -- and is removed.
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
The supervisor was a hand-rolled polling, semaphore, and wait-group
loop predating the project's adoption of the shared worker kit. Two
sibling workers in pkg/probo already use the kit, and go-worker.md
documents it as the project convention.
This commit introduces agentRunHandler, which implements
worker.Handler[coredata.AgentRun] and worker.StaleRecoverer, and
reduces AgentRunSupervisor to a thin wrapper that owns the handler
plus a worker.Worker and bridges ctx cancellation into a handler-
level shutdown broadcast via context.AfterFunc. The agent stop
channel is now closed by a per-Process forwarder goroutine when the
broadcast fires, so in-flight runs checkpoint at the next turn
boundary and drain through wg.Wait before Run returns.
The stop_requested column, struct field, supporting SQL, and the
LoadRunningStopRequestedIDs function are removed end-to-end. None
of it was ever wired to an external surface; it existed purely to
let the supervisor find runs the operator wanted to halt. With the
kit handling the polling cadence and the AfterFunc bridging
shutdown, per-row flagging is dead weight.
The supervisor's public API (NewAgentRunSupervisor, Run, the With*
option helpers, and the error sentinels) stays intact so probod.go
needs no change. The integration test now triggers stop by
cancelling the supervisor context, which is the actual production
path through SIGTERM rather than a synthetic DB flag. Prometheus
counters and OTel spans labelled worker="agent-run-supervisor"
come for free.
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
executeParallel ignored SuspendedError when checkpoint was nil,
treating it as a normal tool error. Nested suspension propagation
also dropped the in-memory checkpoint when persistence failed,
making runs non-resumable.
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
The test does not belong in the probo package. Move it
alongside its shared helpers in pkg/agentruntest.
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
Tests the checkpoint persistence and supervisor lifecycle against a
real Postgres database:
- PGCheckpointStore Save/Load/Delete round-trip
- Supervisor claims PENDING run and completes it
- Cooperative stop/resume via stop_requested flag
- SIGTERM battle test: 3 kill/resume cycles across 10 tool-call
turns with parallel calls, long-running tools, thinking text,
and progressive checkpoint accumulation
Tests skip gracefully when Postgres is unavailable.
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
Poll-based supervisor that claims PENDING agent runs with FOR UPDATE
SKIP LOCKED, runs them with lease-based heartbeat, and handles
graceful shutdown. On infrastructure stop the row stays RUNNING so
stale recovery resets it to PENDING on restart; Restore picks up
from the last checkpoint. Heartbeat loss cancels execution without
committing a terminal status.
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
Create agent_runs table with lease-based concurrency control.
AgentRun entity follows standard coredata patterns with Scoper,
StrictNamedArgs, and cursor pagination. PGCheckpointStore implements
agent.CheckpointStore backed by the checkpoint JSONB column with
version validation and 10 MiB size guard. Register AgentRunEntityType
as entity type 75.
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
Restore loads a checkpoint from the store, resolves the agent from
a registry, and re-enters coreLoop. Handles suspended, nested
suspended (concurrent inner restore), and awaiting-approval states.
Partial progress is saved when some inner agents complete while
others remain suspended.
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
coreLoop now saves incremental checkpoints after each tool-call turn
and checks a cooperative stop signal at turn boundaries. SuspendedError
is handled in finishRun, executeParallel, and executeSingleTool.
Approval-interrupted checkpoints are persisted for both flat and
nested interruptions.
Introduce RunOption, WithCheckpointStore, RunWithOpts, ResumeWithOpts,
and RunStreamedWithOpts so callers can provide checkpoint storage.
Add StreamEventSuspended and OnRunRestore hook.
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
Introduce Checkpoint, CheckpointStore, SuspendedError, AgentRegistry,
and CompletedCall types. Add cooperative stop signal via context.
Export CompletedCall (was unexported completedCall) so checkpoints
can reference completed tool results. Add JSON tags to ToolResult
and ApprovalResult for checkpoint serialization.
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
Message, Part (Text/Image/File), ToolCall, FunctionCall, and Usage
now round-trip through JSON. Message uses a type-discriminated
envelope for the Part interface. Required for checkpoint persistence.
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
The consent cookie is automatically created as a database record in
the necessary category when a new cookie banner is set up, so it
appears alongside other necessary cookies in the banner UI.
Signed-off-by: Émile Ré <emile@getprobo.com>
Include a `bid` field in the consent cookie so it explicitly
identifies which cookie banner it belongs to, making validation
direct instead of relying on the visitor ID as an implicit
discriminator. Existing cookies without `bid` self-heal on the
next load by falling through to the API fetch.
Signed-off-by: Émile Ré <emile@getprobo.com>
Introduce a required cookie_policy_url alongside the existing
privacy_policy_url (now optional) so banners can link directly to a
dedicated cookie policy — a compliance best practice recommended by
CNIL, ICO, and the EDPB. Existing rows are seeded from their current
privacy_policy_url value.
Both {{cookie_policy_link}} and {{privacy_policy_link}} placeholders
are supported independently in banner description translations.
Signed-off-by: Émile Ré <emile@getprobo.com>
Origin is a fundamental identity property of a banner tied to consent
records for a specific site. Changing it would break the audit trail
and violate GDPR consent specificity requirements.
Signed-off-by: Émile Ré <emile@getprobo.com>
The SCIM bridge for Google Workspace already exists but the
bootstrap builder did not register it, preventing deployment
via environment variables.
Signed-off-by: Bryan Frimin <bryan@getprobo.com>
When DNS and CAA checks pass, ProvisioningError is set to nil but
was only persisted later alongside the challenge data. If
GetHTTPChallenge then failed, the update was never reached,
leaving stale DNS/CAA error messages visible to the user.
Signed-off-by: Bryan Frimin <bryan@getprobo.com>
When DNS verification, CAA checks, or HTTP challenge completion
fail during certificate provisioning, the error is now stored on
the custom domain record and exposed via GraphQL. The console
displays it in both the domain card and domain detail dialog so
users can diagnose configuration issues without checking logs.
Previously these failures returned an error that was only logged
server-side, leaving users with no visibility into why their
domain was stuck in a pending state.
Signed-off-by: Bryan Frimin <bryan@getprobo.com>
Add a partial unique index ensuring only one category per banner can
have posthog_consent enabled. Default it to the analytics category on
banner creation, clear the previous mapping before setting a new one,
and restrict the toggle to NORMAL categories in both the service layer
and the console UI.
Signed-off-by: Émile Ré <emile@getprobo.com>
Add PostHog opt-in/opt-out consent support mirroring the existing Google
Consent Mode integration: database column, GraphQL field, console UI
toggle, and client-side posthog-js calls.
Extract both GCM and PostHog logic from CookieBannerClient into a
ConsentIntegration plugin interface so future integrations can be added
without modifying the client core.
Signed-off-by: Émile Ré <emile@getprobo.com>
Expose gcmConsentTypes on CookieCategory type and accept
it in UpdateCookieCategoryInput so the console can read
and write GCM consent type mappings per category.
Signed-off-by: Émile Ré <emile@getprobo.com>
Include gcm_consent_types in the published banner config
so the cookie banner SDK can read category-to-GCM mappings
from the config endpoint.
Signed-off-by: Émile Ré <emile@getprobo.com>
Stores the Google Consent Mode v2 consent type mapping
per cookie category so each category can declare which
GCM signals it controls (e.g. analytics_storage,
ad_storage).
Signed-off-by: Émile Ré <emile@getprobo.com>
Address locale normalization for region-tagged values, guard
language detection for non-DOM runtimes, validate DefaultLanguage
on update, pass translated texts through the deactivation flow,
handle slug collisions in migration, add organizations FK, fix
consent migration from name-keyed to slug-keyed data, render all
template placeholders in previews, and wrap helper text for i18n.
Signed-off-by: Émile Ré <emile@getprobo.com>
label_description and label_duration are not user-customizable, so they
don't belong in the backend translation defaults. Hardcode them in the
cookie-banner SDK's i18n module instead.
Signed-off-by: Émile Ré <emile@getprobo.com>
The mutation was wired up in the GraphQL schema and resolver
but never called from the console frontend. This removes the
service method, request type, resolver, schema entries, and
two unused error sentinels (ErrTranslationNotFound,
ErrTranslationAlreadyExists).
Signed-off-by: Émile Ré <emile@getprobo.com>
Validate translation string values server-side with NoHTML() and
MaxLen(2000) to reject HTML in the translations JSON blob. On the
client side, escape user-provided template text before innerHTML
injection in banner_description and placeholder_text paths.
Signed-off-by: Émile Ré <emile@getprobo.com>
Introduce category-level name/description translations in the cookie
banner i18n flow. Seed default translations for fr/de/es on banner
creation, parse them from the stored JSON, and manage them via
react-hook-form Controllers instead of a manual ref/callback pattern.
Enhance the panel preview with category descriptions and all three
action buttons.
Signed-off-by: Émile Ré <emile@getprobo.com>
The category slug provides a stable, URL-safe key used as the
data-cookie-consent attribute value and consent data key, replacing
the fragile category name. This prevents breakage when categories
are renamed.
- Add slug column with unique-per-banner constraint and backfill migration
- Add Slug validator (lowercase alphanumeric + hyphens)
- Propagate slug through GraphQL schema, service layer, and snapshot
- Update console UI with slug field in create/edit forms
- Switch cookie-banner widget to use slug as consent data keys
Signed-off-by: Émile Ré <emile@getprobo.com>
Duration strings are static and not customizable through the console,
so they belong in the frontend cookie-banner package instead.
Signed-off-by: Émile Ré <emile@getprobo.com>
The field was populated by the backend and declared in the TS interface
but never consumed by any client-side code. The banner resolves its
language via detectLanguage() and passes it as a query param.
Signed-off-by: Émile Ré <emile@getprobo.com>
New banners now get French, German, and Spanish UI string
translations seeded alongside English during creation.
Signed-off-by: Émile Ré <emile@getprobo.com>
Add CookieBannerTranslation type, defaultLanguage field on
CookieBanner, and upsert/delete mutations for managing
per-language translations from the console.
Signed-off-by: Émile Ré <emile@getprobo.com>
Pass the lang parameter from the request URL to the service
so the config response returns localized texts and category
names for the requested language.
Signed-off-by: Émile Ré <emile@getprobo.com>
Seed default English UI strings on banner creation, include
translations in version snapshots, and resolve language at
config-serving time. Add translation CRUD methods and the
language-aware buildBannerConfig helper. Extract default
categories and UI strings into defaults.go.
Signed-off-by: Émile Ré <emile@getprobo.com>
Add cookie_banner_translations table to store per-language
translations as JSONB, and a default_language column on
cookie_banners. Extend the version snapshot types to carry
translated UI strings and category content per language.
Signed-off-by: Émile Ré <emile@getprobo.com>
The rebase onto main split the evidence-describer config into two: an
LLMAgentConfig under agents.evidence-describer and a worker-tuning
EvidenceDescriberConfig (Interval, StaleAfter, MaxConcurrency) at the
top level. Bootstrap was only populating the LLM half, leaving Interval
at zero — which panics the probod boot with "non-positive interval for
NewTicker" when the evidence description worker starts. Add the missing
fields with the same defaults main uses.
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
Move PDF generation from synchronous publish flow to a background polling
job. Published versions with file_id IS NULL are picked up by the job,
which generates the PDF, uploads to S3, and links the file. Export PDF
now serves stored files for published versions (with optional signature
page and watermark) and generates on the fly for drafts.
Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
When a user's email is renamed in the identity provider (e.g. Google
Workspace), the external ID stays the same but the email changes. The
SCIM CreateUser now falls back to external ID lookup when no profile is
found by identity, and reassociates the existing profile to the new
identity instead of failing with a 409 uniqueness error.
Also removes user emails from bridge sync error messages to avoid
logging PII, using external IDs instead.
Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>