Commit Graph

1755 Commits

Author SHA1 Message Date
Aurélien Sibiril
bfa1c21723 Document AgentRun.Update checkpoint write split
Update writes every mutable column of agent_runs except checkpoint.
That exclusion is intentional: PGCheckpointer.Save and ClearCheckpoint
are the only paths that touch the column, so a status commit cannot
overwrite an in-flight checkpoint saved between Load and Update.

Surface the rule on the Update method so future readers do not patch
in a checkpoint write thinking it was an oversight.

Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
2026-04-26 22:58:22 +02:00
Aurélien Sibiril
119638deda Skip LastAgent in Result JSON serialization
Result.LastAgent is *Agent, which holds only unexported fields. The
default json.Marshal renders it as an empty object, which is misleading
when persisted alongside the agent run.

Tag LastAgent as json:"-" and add explicit lowercase JSON tags to the
remaining fields so the serialized shape is stable for callers that
read the persisted result column.

Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
2026-04-26 22:58:22 +02:00
Aurélien Sibiril
e5b354c3b4 Collapse RunWithOpts duplicates into variadic options
Run, RunStreamed, and Resume each shipped both a no-options form and a
mirror *WithOpts form taking variadic RunOption. Variadic parameters
are backward-compatible additions, so the wrappers were dead surface.

Make Run, RunStreamed, and Resume directly variadic and update the two
internal callers.

Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
2026-04-26 22:58:22 +02:00
Aurélien Sibiril
feeb6496b9 Polish supervisor integration tests
Documents why the supervisor suite stays sequential (claim is
cross-tenant via LoadNextPendingForUpdateSkipLocked; parallel
supervisors would steal each other's runs) and switches the SIGTERM
subtest to errors.AsType[*exec.ExitError] to match the project
convention enforced elsewhere in this file.

Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
2026-04-26 22:58:22 +02:00
Aurélien Sibiril
20dbc28398 Expose supervisor shutdown broadcast for tests
Adds AgentRunSupervisor.ShutdownBroadcast() returning the handler's
shutdown channel so the StopAndResume integration test can wait for
graceful-shutdown propagation deterministically instead of sleeping
for a fixed duration. The method is explicitly documented as
test-only and not part of the operational contract.

Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
2026-04-26 22:58:22 +02:00
Aurélien Sibiril
6e3de55173 Truncate persisted agent run errors on rune boundary
Raw tool or LLM errors can embed URLs with credentials, PII, or
partial DB records. We now log the full runErr for operator context
and persist a sanitized summary into agent_runs.error_message,
truncated at 512 bytes with a trailing ellipsis. The cut rewinds to
the nearest utf8.RuneStart so we never store a split rune. Also
wraps the pending-run loader error in Claim for parity with the
other coredata failures.

Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
2026-04-26 22:58:22 +02:00
Aurélien Sibiril
cc579b1ed4 Parallelize PG checkpointer subtests
Each subtest now inserts its own PENDING run and runs under
t.Parallel(); shared state across subtests was the only reason they
had to stay sequential. Also adds a round-trip test that exercises
the approval-state fields (PendingToolCalls, PendingApprovals,
ApprovalInput, AllToolCalls, InnerCheckpoints, CompletedCalls) to
catch regressions where Save/Load drops nested or approval payloads.
The nonexistent-run case now uses a valid GID in the same tenant so
it reaches the row-not-found branch instead of short-circuiting on
the tenant-scope guard.

Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
2026-04-26 22:58:22 +02:00
Aurélien Sibiril
33119f9306 Scope PGCheckpointer queries by tenant GID
Each Save and Load now derives tenant_id from the run GID and pins it
in the WHERE clause. A caller that supplies an ID from another tenant
fails closed instead of silently reading or overwriting cross-tenant
checkpoint data. Also rejects oversize checkpoints on load as a
read-side guard against a tampered or migrated row exceeding
MaxCheckpointBytes.

Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
2026-04-26 22:58:21 +02:00
Aurélien Sibiril
70139a9210 Drop unused AgentRun.SaveCheckpoint
The transactional variant is no longer reachable: supervisor paths
persist checkpoints through PGCheckpointer (WithConn, not WithTx),
and Update deliberately excludes the checkpoint column to avoid
racing a concurrent checkpoint save. Nothing else calls it.

Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
2026-04-26 22:58:21 +02:00
Aurélien Sibiril
3cebf7a1df Refine tool dispatch errors in resume path
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>
2026-04-26 22:58:21 +02:00
Aurélien Sibiril
d13c83c19f Snapshot MaxTurns in checkpoint and apply it on restore
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>
2026-04-26 22:58:21 +02:00
Aurélien Sibiril
6887294c9e Rebuild AgentRunSupervisor on go.gearno.de/kit/worker
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>
2026-04-26 22:58:21 +02:00
Aurélien Sibiril
fb88318c54 Fix suspension checkpoint fallback in nested and parallel execution
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>
2026-04-26 22:58:21 +02:00
Aurélien Sibiril
83dba57add Move supervisor test to agentruntest package
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>
2026-04-26 22:58:21 +02:00
Aurélien Sibiril
71e2d00b3a Rename CheckpointStatus to AgentStatus
The status values describe the agent state, not the
checkpoint data state.

Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
2026-04-26 22:58:21 +02:00
Aurélien Sibiril
ff18a5fc23 Remove json tags from internal structs
JSON marshaling uses field names directly; explicit tags
are unnecessary at this level.

Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
2026-04-26 22:58:21 +02:00
Aurélien Sibiril
7058d310b0 Add agent run supervisor integration tests
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>
2026-04-26 22:58:21 +02:00
Aurélien Sibiril
e831ee3c63 Add agent run supervisor worker
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>
2026-04-26 22:58:21 +02:00
Aurélien Sibiril
2f2b8fe6aa Add AgentRun entity and PG checkpoint store
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>
2026-04-26 22:58:21 +02:00
Aurélien Sibiril
363d696906 Add Restore function for agent checkpoint recovery
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>
2026-04-26 22:58:21 +02:00
Aurélien Sibiril
7d5b933aa4 Add checkpoint persistence to agent core loop
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>
2026-04-26 22:58:21 +02:00
Aurélien Sibiril
51df618c93 Add checkpoint types and stop signal for agent suspension
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>
2026-04-26 22:58:21 +02:00
Aurélien Sibiril
19a2610056 Add JSON serialization to LLM message types
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>
2026-04-26 22:58:21 +02:00
Émile Ré
dd42affd98 Drop SQL column defaults after migration for cookie_policy_url and sdk_version
Signed-off-by: Émile Ré <emile@getprobo.com>
2026-04-24 19:01:27 +04:00
Émile Ré
351cd49162 Add probo_consent cookie to necessary category on banner creation
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>
2026-04-24 18:48:40 +04:00
Émile Ré
d7eec08cd2 Add banner ID to probo_consent cookie
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>
2026-04-24 18:48:40 +04:00
Émile Ré
598c6b112c Add cookie_policy_url field to cookie banners
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>
2026-04-24 18:48:39 +04:00
Émile Ré
11f856740a Make cookie banner origin immutable after creation
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>
2026-04-24 18:48:39 +04:00
Bryan Frimin
5bd31bd68f Add Google Workspace connector to bootstrap
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>
2026-04-24 16:06:35 +02:00
Bryan Frimin
da9ba64b07 Persist cleared provisioning error before HTTP challenge
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>
2026-04-24 16:05:11 +02:00
Bryan Frimin
976acbd114 Surface domain provisioning errors to users
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>
2026-04-24 16:05:11 +02:00
Émile Ré
e48da4dc99 Format file
Signed-off-by: Émile Ré <emile@getprobo.com>
2026-04-24 17:16:04 +04:00
Émile Ré
9fbb716b00 Constrain PostHog consent to one normal category per banner
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>
2026-04-24 17:13:12 +04:00
Émile Ré
7f1dffad80 Add PostHog consent integration and extract integration plugin system
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>
2026-04-24 17:13:12 +04:00
Émile Ré
4436059483 Add gcmConsentTypes to GraphQL schema and resolvers
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>
2026-04-24 17:13:11 +04:00
Émile Ré
60705af3a2 Add default GCM consent type mappings
Seed default categories with their Google Consent Mode v2
mappings (e.g. analytics -> analytics_storage, advertising
-> ad_storage + ad_user_data + ad_personalization). New
user-created categories default to an empty mapping.

Signed-off-by: Émile Ré <emile@getprobo.com>
2026-04-24 17:13:11 +04:00
Émile Ré
5112233925 Add GCM consent types to version snapshot
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>
2026-04-24 17:13:11 +04:00
Émile Ré
2b15a1684c Add gcm_consent_types column to cookie_categories
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>
2026-04-24 17:13:11 +04:00
Sacha Al Himdani
ab5654f6e5 Fix errcheck and ineffassign lint errors
Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
2026-04-24 14:24:48 +02:00
Émile Ré
2b6f131f43 Fix PR review comments on cookie banner i18n
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>
2026-04-24 12:18:12 +04:00
Émile Ré
fa7a9c96c1 Move cookie detail labels from backend to JS SDK
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>
2026-04-24 12:18:12 +04:00
Émile Ré
0b7855243a Remove unused deleteCookieBannerTranslation mutation
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>
2026-04-24 12:18:11 +04:00
Émile Ré
e24813202b Fix XSS in cookie banner translation rendering
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>
2026-04-24 12:18:11 +04:00
Émile Ré
cd824c2c55 Add per-category translations and improve preferences panel preview
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>
2026-04-24 12:18:11 +04:00
Émile Ré
165b9ad9d3 Add slug to cookie categories for stable consent identifiers
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>
2026-04-24 12:18:10 +04:00
Émile Ré
bb39daebc5 Move duration translations from server defaults to JS SDK
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>
2026-04-24 12:18:10 +04:00
Émile Ré
fcd58b0f48 Format file
Signed-off-by: Émile Ré <emile@getprobo.com>
2026-04-24 12:18:10 +04:00
Émile Ré
f9458fb18a Remove unused available_languages from cookie banner config
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>
2026-04-24 12:18:09 +04:00
Émile Ré
1361dda425 Fix gofmt formatting
Signed-off-by: Émile Ré <emile@getprobo.com>
2026-04-24 12:18:09 +04:00
Émile Ré
1ff1901331 Add default cookie banner translations for fr, de, es
New banners now get French, German, and Spanish UI string
translations seeded alongside English during creation.

Signed-off-by: Émile Ré <emile@getprobo.com>
2026-04-24 12:18:09 +04:00