Skip to content

Compliance risk agent ​

This page explains how a compliance risk agent, under controlled authority, patrols owned sites and marketing materials for wording that touches forbidden claims or regulatory red lines, and combines public regulatory announcements into an evidence-backed risk list. After reading it, you will understand which modules this scenario needs, when interactive consent is actually needed, and why forbidden-claims lists and regulation versions must come from a controlled policy store rather than Memory.

Use case ​

Compliance teams need to monitor regulatory announcements, policy changes, industry risks, and business impact, while ensuring that wording on owned websites, landing pages, and marketing materials does not touch forbidden claims or regulatory red lines. Material volume is large and updates are frequent; manual page-by-page checks cannot keep up with the release pace, and once a regulator's letter arrives, reconstructing "what the page said at the time" is often impossible.

Typical triggers:

  • A regulator publishes new wording restrictions or industry guidance, and existing materials must be screened for impact.
  • Before a major campaign launches, all landing pages and materials must be patrolled for wording.
  • A periodic compliance patrol cycle is due, and this period's risk list and evidence archive must be produced.

Engineering challenges ​

  • The judgment basis must be controlled: forbidden-claims lists and regulation entries are compliance assets with versions and effective dates. Any rule "recalled from memory" may be stale wording — a conclusion built on the wrong list is more dangerous than a missed item.
  • Evidence requirements exceed a normal patrol: a regulator's follow-up needs to reconstruct "what the page said, judged against which list version"; a suspected item without a screenshot, verbatim excerpt, and timestamp has no evidentiary value.
  • The output's positioning is sensitive: once a tool's output is treated as a compliance verdict, it displaces the professional judgment that belongs to the compliance team — the output must stay strictly an audit data foundation, never a conformity assertion.

Module composition ​

ModuleRoleNotes
GenAuthCoreSilent delegation issues the short-lived runtime credential every product call requires; interactive consent is only needed when login state or write actions enter the picture. Credentials are re-issued per cycle and revocable.
Web AgentCorePatrols owned sites and material pages with Track, comparing wording against the forbidden-claims list with deterministic rules; searches regulator sites and public legal materials through WebSearch, keeping all sources.
GUMemNot usedForbidden-claims lists and regulation versions are controlled compliance assets — they must come from your controlled policy store and be cited by version. Risk lists, evidence, and the compliance team's final judgments are archived to your audit storage. Neither is Memory.

Compliance risk agent architecture

Every product call requires a GenAuth delegate token; public read-only scenarios are covered by silent delegation. The main targets in this scenario are owned public pages and public regulatory sources, which silent delegation (delegateToken without mode: 'interactive') covers: credentials are short-lived, revocable at any time, and re-issued per patrol cycle. Only the following cases require escalating to interactive consent, confirmed by the compliance owner in Qoni Console:

  • The patrol needs a signed-in materials back office or CMS preview.
  • The delegation scope expands to a new business line or region.

Note: this sample requests doAnything product permissions. Your application and downstream services must configure and enforce business limits such as business lines, regions, page ranges. Product scopes and prompt rules do not enforce those detailed limits; this sample does not configure them. See Delegate token and attenuation.

Workflow ​

Compliance risk agent workflow

  1. The compliance team submits the patrol scope: business line, region, and material list.

  2. GenAuth issues a read-only delegation credential for this patrol cycle.

  3. Your app loads the current version of the forbidden-claims list and applicable regulation entries from the controlled policy store and injects them into the task.

  4. Web Agent searches related regulatory announcements and legal updates through WebSearch, recording sources.

  5. Web Agent patrols owned sites and material pages with Track, comparing wording against the forbidden-claims list with deterministic rules.

    Checkpoint: Every suspected violation must retain a page screenshot, verbatim excerpt, and source URL; suspected items without evidence do not enter the risk list.

  6. Your app validates the output contract — suspected items missing an evidence source or rule ID are dropped — and archives the risk list and evidence pointers to your audit storage by list version.

  7. The Agent returns the risk list, evidence, items pending human confirmation, and audit id, and hands them to the compliance team.

    Checkpoint: The deliverable should state explicitly that it "provides an audit data foundation and makes no compliance conformity assertion"; the final judgment belongs to the compliance team.

Example code ​

This example uses @qoniai/qoni 0.9.0, published on npm. The download includes the same SDK version, installed with npm ci. The demo reviews Firefox public privacy information against a synthetic rule requiring evidence for absolute claims. The SDK reads real public pages; business inputs in scenarios.ts are labeled public-demo.

This demo does not read or write GUMem; its task uses the supplied page list and explicit business inputs. DoAnything opens the supplied pages and produces the scenario output. Supply your own JSON with --input; use --interactive when the user must consent to delegation. For site sign-in and user responses, see Qoni SDK.

Download the complete runnable examples, or run from the documentation repository:

bash
cd examples/qoni
npm ci
npm run case -- compliance-risk-agent
# Supply your own inputs
npm run case -- compliance-risk-agent --input /path/to/input.json

Set server-side QONI_ACCESS_KEY and QONI_SECRET_KEY. QONI_USER_ID can identify your application's current GenAuth user; the local demo otherwise selects a user from the bound pool. Demonstration Memory writes use an isolated user rather than changing a business user's preferences.

This scenario's executable entry point:

ts
import { cliOptions } from '../runtime.js'
import { inputFile, runScenario } from '../run-case.js'

// Cite page evidence in a rule-review draft for a human reviewer.
const report = await runScenario('compliance-risk-agent', cliOptions(), inputFile())
// report.items: pages, risks or observations, quotations, rule IDs, and sources; this is not legal advice.
// Validated fields: page, risk, quote, ruleId, sourceUrl.
console.log(JSON.stringify(report, null, 2))

The entry point loads the definition below by scenario ID. The code is included directly from scenarios.ts, with comments shown in the page language: the task, output fields, source field, Web Search queries (if any), whether Memory is used, and the demonstration input. The pipeline appends the input data, search sources, recalled Memory, and shared safety constraints to the task to build the final prompt; see the pipeline below for the full assembly.

ts
// Cite page evidence in a rule-review draft for a human reviewer.
browser('compliance-risk-agent',
  'Review the supplied public pages against the demonstration policy. Return [{page,risk,quote,ruleId,sourceUrl}]. This is a rule-checking draft for a reviewer, not a legal conclusion. Record inspected evidence when no issue is found. Do not edit content or make reports to third parties.',
  ['page','risk','quote','ruleId','sourceUrl'], 'sourceUrl', sample([privacy], { rules:[{id:'absolute-claims',text:'Absolute claims require explicit evidence.'}] })),

The sample implements runScenario(), browser(), research(), and sample() as application functions. The pipeline below makes the actual SDK calls: delegation and introspection → required Memory/search → browser task or monitor → validation and saving. fields and sourceField define the application's output checks. The complete application helpers are in the package's runtime.ts.

Inspect the actual SDK pipeline
ts
// browser() uses DoAnything; research() searches first; sample() labels public-demo inputs.
const firefox = 'https://www.mozilla.org/en-US/firefox/new/'
const manifesto = 'https://www.mozilla.org/en-US/about/manifesto/'
const privacy = 'https://www.mozilla.org/en-US/privacy/firefox/'
const support = 'https://support.mozilla.org/en-US/kb/get-started-firefox-overview-main-features'
// These copy rules become prompt context; server permissions and business checks remain separate.
const policy = {
  version: 'demo-2026-09',
  approvedClaims: ['Describe only features supported by the cited page.'],
  forbiddenClaims: ['guaranteed security', '100% private', 'unverified pricing or performance'],
  voice: 'concise and warm',
}
const sample = (pages: string[], business: JsonObject = {}): JsonObject => ({
  dataset: 'public-demo', pages, policy, business,
  notice: 'Business records are synthetic demonstration inputs. Referenced websites and SDK execution are real.',
})
// fields lists required output keys; sourceField identifies URL checks; memory enables GUMem calls.
const browser = (id: string, task: string, fields: string[], sourceField: string | undefined, input: JsonObject, memory = false): Scenario => ({
  id, products: ['doAnything'], task, fields, sourceField, input, memory,
})
const research = (id: string, task: string, fields: string[], sourceField: string, queries: string[], input: JsonObject, memory = false): Scenario => ({
  id, products: ['webSearch', 'doAnything'], task, fields, sourceField, queries, input, memory,
})
ts
import { QoniScopes, type JsonObject, type RunResult } from '@qoniai/qoni'
import { readFileSync } from 'node:fs'
import { getScenario, type Scenario } from './scenarios.js'
import { appendTrace, checkInputCoverage, cleanupDemoUser, createContext, delegate, handleInteraction, inputEntryCount, isolateDemoUser, object, readWithRetry, renderScreenshot,
  save, saveArtifacts, searchHits, settled, settleRun, validateItems, withCleanup, type Context, type Options } from './runtime.js'

export async function runScenario(id: string, options: Options = {}, input?: JsonObject) {
  // Load the task definition by ID; --input replaces its business inputs.
  const scenario = getScenario(id)
  const data = input ?? scenario.input
  if (scenario.memory && data.dataset !== 'public-demo' && options.mode !== 'interactive' && !options.userId && !process.env.QONI_USER_ID) {
    throw new Error('Business Memory writes require the current QONI_USER_ID; do not select an arbitrary bound user')
  }
  const context = await createContext(id, { ...options,
    skipUserResolution: scenario.memory && data.dataset === 'public-demo' })
  return withCleanup(context, async register => {
    register('isolated demonstration user', () => cleanupDemoUser(context))
    // Isolate demo preferences; business Memory belongs to the identified current user.
    if (scenario.memory && data.dataset === 'public-demo') await isolateDemoUser(context)
    return await executeScenario(context, scenario, data)
  })
}

export async function executeScenario(context: Context, scenario: Scenario, input: JsonObject) {
  const pages = input.pages
  if (!Array.isArray(pages) || !pages.length || pages.some(page => typeof page !== 'string' || !/^https:\/\//.test(page))) {
    throw new Error('Input pages must be an array of HTTPS URLs')
  }
  if (input.requiresLogin === true && context.mode !== 'interactive') {
    throw new Error('Targets that require sign-in need --interactive and user-controlled login')
  }
  const memoryScopes = scenario.memory
    ? [QoniScopes.GUMEM_MEMORY_READ, QoniScopes.GUMEM_MEMORY_WRITE, QoniScopes.GUMEM_MESSAGE_WRITE] : []
  // delegate() is an application helper around the SDK delegation methods.
  const grant = await delegate(context, scenario.id, scenario.products, memoryScopes)
  // Read the effective scopes; readWithRetry() retries only retryable read failures.
  const { data: tokenInfo } = await readWithRetry(context, 'delegation introspection',
    () => context.qoni.genauth.introspectDelegationToken({ token: grant.token }))
  const info = object(tokenInfo)
  if (info.active !== true) throw new Error('The delegation token is not active')
  const audit = { grantId: grant.grantId, auditId: grant.auditId, scopes: info.scope }
  let memory: unknown
  if (scenario.memory) {
    // A Session associates this conversation with the user; the app chooses sessionId.
    const sessionId = `${scenario.id}-${Date.now()}`
    await context.qoni.gumem.createSession({
      token: grant.token, userId: context.userId, sessionId, title: scenario.id,
    })
    const preferences = object(input.business ?? {}).confirmedPreferences
    if (Array.isArray(preferences) && preferences.length) {
      // Store confirmed preferences only; sync: true requests synchronous processing.
      await context.qoni.gumem.addMessages({ token: grant.token, userId: context.userId, sessionId, sync: true,
        messages: [{ role: 'user', content: `Confirmed demonstration preferences: ${preferences.join('; ')}` }] })
    }
    // Recall relevant preferences for the later task prompt.
    memory = (await readWithRetry(context, 'GUMem recall', () => context.qoni.gumem.recall({ token: grant.token, sessionId,
      query: 'Confirmed preferences relevant to this task', details: true }))).data
    save(context, 'memory.json', { sessionId, context: memory })
    if (Array.isArray(preferences) && preferences.length && !preferences.every(value => JSON.stringify(memory).includes(String(value)))) {
      throw new Error('Recall did not include the confirmed preferences just written by this demo')
    }
  }

  if (scenario.products.includes('track')) return runMonitor(context, scenario, input, grant.token, audit)

  let hits: ReturnType<typeof searchHits> = []
  if (scenario.queries) {
    // Web Search returns results[]; DoAnything receives these sources to inspect.
    const search = await context.qoni.webSearch.run({ token: grant.token, prompt: scenario.queries, maxResultsPerQuery: 3 })
    save(context, 'search-ref.json', { runId: search.id, audit })
    await withCleanup(context, async register => {
      register('Web Search run', () => search.cancel('Documentation demonstration cleanup'))
      const result = await settleRun(context, search)
      save(context, 'search-result.json', result)
      settled(result)
      hits = searchHits(result.output)
    })
  }

  // One-per-entry scenarios request exactly one item per input entry; others at most two.
  const requiredItems = inputEntryCount(scenario.id, input)
  // Assemble the task, inputs, search sources, and Memory as application-defined context.
  const prompt = [scenario.task, `Task inputs: ${JSON.stringify(input)}`,
    `Search sources: ${JSON.stringify(hits)}`, `Confirmed memory: ${JSON.stringify(memory ?? null)}`,
    `Actual collection time: ${new Date().toISOString()}`,
    requiredItems === undefined
      ? 'Inspect the supplied sources. Return at most two items in the requested JSON array, without prose or Markdown.'
      : `Inspect the supplied sources. Return exactly ${requiredItems} item${requiredItems === 1 ? '' : 's'} in the requested JSON array, one per input entry, without prose or Markdown.`,
    'Keep synthetic demonstration data identified as synthetic. Do not send messages, publish, pay, edit accounts or submit forms.',
    input.requiresLogin === true ? 'Request user sign-in through an interaction when required; never enter credentials yourself.' : 'Public demonstration sources only; do not sign in.',
  ].join('\n\n')
  // Start the Agent with this grant; capture receives delivered screenshots, not every step.
  const run = await context.qoni.doAnything.run({ token: grant.token, prompt, capture: { screenshots: true } })
  save(context, 'run-ref.json', { runId: run.id, session: run.sessionRef, audit })
  let result: RunResult
  const trace = (event: { type: string; data: unknown }) => {
    context.eventCounts[event.type] = (context.eventCounts[event.type] ?? 0) + 1
    if (['progress','message','done'].includes(event.type)) appendTrace(context, event)
    if (event.type === 'browserLiveUrlChanged') {
      const liveUrl = object(event.data).liveUrl
      if (typeof liveUrl === 'string') context.browserUrl = liveUrl
    }
  }
  return withCleanup(context, async register => {
    register('DoAnything run', () => run.cancel('Documentation demonstration cleanup'))
    if (context.delivery === 'events') {
      // --events streams updates; wrap interaction data in an SDK handle for user handling.
      for await (const event of run.events({ signal: AbortSignal.any([context.abort.signal, AbortSignal.timeout(context.timeoutMs)]) })) {
        trace(event)
        if (event.type === 'screenshot') renderScreenshot(context, event.image)
        if (event.type === 'interaction') await handleInteraction(context, run.interactionHandle(event.data))
      }
      result = await settleRun(context, run)
    } else {
      // Callback mode receives this run's events inside wait; helpers save images and ask the user.
      result = await settleRun(context, run, { onEvent: trace,
        onScreenshot: (image, index) => renderScreenshot(context, image, index),
        onInteraction: interaction => handleInteraction(context, interaction) })
    }
    save(context, 'result.json', result)
    settled(result)
    await saveArtifacts(context, result)
    if (scenario.id === 'landing-page-audit-agent' && context.screenshots === 0) throw new Error('The landing-page audit did not deliver the requested screenshot')
    // The app checks required fields and source URL formats; a reviewer still checks facts.
    const items = validateItems(result.output, scenario.fields, scenario.sourceField)
    // Scenarios that require one item per input entry are checked against the input.
    checkInputCoverage(scenario.id, items, input)
    const report = { scenario: scenario.id, dataset: input.dataset, passed: true, runId: run.id,
      status: result.status, items, audit, screenshots: context.screenshots, interactions: context.interactions,
      events: context.eventCounts, artifactIds: result.artifacts.map(artifact => artifact.id) }
    save(context, 'report.json', report)
    return report
  })
}

async function runMonitor(context: Context, scenario: Scenario, input: JsonObject, token: string, audit: JsonObject) {
  // Track creates a monitor with targets, extraction fields, and hourly scheduling.
  const monitor = await context.qoni.track.create({ token, prompt: scenario.task,
    targetUrls: input.pages, extractionSchema: { heading: 'string', source_url: 'string' },
    tickInstructions: `Open the target URLs and read the actual visible heading. Return a JSON object with heading and source_url. ${scenario.task}`,
    triggerDsl: { on: 'change' }, schedule: { kind: 'interval', intervalSeconds: 3600 } })
  save(context, 'monitor-ref.json', { id: monitor.id, audit })
  return withCleanup(context, async register => {
    register('Track monitor', () => monitor.delete())
    const definition = await monitor.get()
    save(context, 'monitor-definition.json', definition)
    if (object(definition.schedule).intervalSeconds !== 3600) throw new Error('Track did not persist the requested schedule interval')
    // Run one tick and inspect its extraction by runId; completed alone does not prove success.
    const tick = await monitor.runNow()
    save(context, 'tick.json', tick)
    if (tick.state !== 'completed') throw new Error(`Track execution failed: ${tick.state} / ${tick.error ?? ''}`)
    const runId = tick.runId
    if (typeof runId !== 'string') throw new Error('Track tick did not return a runId')
    const detail = await monitor.run(runId)
    save(context, 'tick-detail.json', detail)
    if (detail.state !== 'completed' || !detail.extracted || !Object.keys(object(detail.extracted)).length) {
      throw new Error('Track did not extract page data')
    }
    const extracted = object(detail.extracted)
    if (typeof extracted.heading !== 'string' || !extracted.heading.trim() ||
      typeof extracted.source_url !== 'string' || !/^https:\/\//.test(extracted.source_url)) {
      throw new Error('Track extraction is missing a heading or source URL')
    }
    const normalizeUrl = (value: string) => { const url = new URL(value); url.hash = ''; return url.href.replace(/\/$/, '') }
    if (!(input.pages as string[]).some(url => normalizeUrl(url) === normalizeUrl(String(extracted.source_url)))) {
      throw new Error('The Track source URL is not a configured target')
    }
    // Check persisted pause/resume state; withCleanup() deletes the monitor on exit.
    await monitor.pause()
    if ((await monitor.get()).status !== 'paused') throw new Error('Track did not persist the paused state')
    await monitor.resume()
    if ((await monitor.get()).status !== 'active') throw new Error('Track did not persist the active state')
    const report = { scenario: scenario.id, dataset: input.dataset, passed: true, monitorId: monitor.id,
      runId, state: detail.state, outcome: detail.outcome, extracted: detail.extracted, audit }
    save(context, 'report.json', report)
    return report
  })
}

export function inputFile(): JsonObject | undefined {
  const index = process.argv.indexOf('--input')
  return index >= 0 ? object(JSON.parse(readFileSync(process.argv[index + 1], 'utf8'))) : undefined
}

Results are written to output/compliance-risk-agent/report.json. report.items contains pages, risks or observations, quotations, rule IDs, and sources; this is not legal advice, with fields page, risk, quote, ruleId, sourceUrl. audit links the grant ID, audit ID, and effective scopes; http.json records redacted request statuses. The application parses DoAnything output and checks required fields and source URL formats. A business reviewer still assesses the content against the original sources.

Data and memory boundaries ​

This scenario touches four kinds of data; none of them needs GUMem:

  • Versioned rules: forbidden-claims lists and regulation entries with effective dates — managed by version in your controlled policy store, with every risk item citing a list version.
  • Business state: risk lists, evidence (screenshots, verbatim excerpts, source URLs), and the compliance team's final judgments — archived by list version to your audit storage for regulatory follow-up.
  • Audit records: the delegation and behavior chain formed by grantId and auditId — maintained by GenAuth.
  • User Memory (optional): this scenario neither recalls nor writes back by default; the compliance judgment basis must come entirely from the controlled policy store, and rules from Memory are not accepted.

Failure handling ​

SituationRecommended handling
A material page requires sign-in and the login state expiresSuspend the task, notify the compliance officer to sign in again, and resume the patrol from the checkpoint.
A regulatory source page is unreachableKeep the failure record and mark related risk items as having incomplete basis; never substitute cached content.
The policy store is unreachable or the list version is missingAbort this cycle and alert; never continue on a cached or previous-cycle list.
Wording is suspicious but rules cannot decide deterministicallyKeep the evidence and mark it as pending human judgment; the Agent must not classify it as violating or compliant on its own.

Production notes ​

Compliance conclusions should include sources and uncertainty. They do not replace professional legal advice. The Agent outputs an audit data foundation — risk lists, evidence, and sources — not a compliance conformity assertion; whether something violates the rules and how to remediate is the compliance team's final call. The patrol is read-only and never edits or takes down any material; every cycle's risk list should bind to the forbidden-claims list version, and old conclusions are not retroactively re-judged after the list changes.

Next steps ​