Skip to main content

Privacy Model

Concepts

Privacy is not an afterthought in Periscope -- it is a core architectural constraint. Every piece of browser context passes through privacy filtering before it reaches AI agents. This page explains the four privacy levels, the two-layer filtering pipeline, the specific patterns that get redacted, and how you can configure filtering behavior for your integration.

Why Privacy Matters Here

Periscope captures browser activity. Browsers are where people enter passwords, fill out tax forms, browse medical information, manage bank accounts, and read private messages. If context data were transmitted without filtering, agents would have access to an unreasonable amount of sensitive information.

The privacy model is designed around a principle: agents should receive the minimum context needed to be helpful, not the maximum context available. The system defaults to conservative filtering and requires explicit configuration to relax it.

Privacy Levels

Every BrowserContext carries a privacyLevel field. This field controls how the context is handled at every stage of the pipeline:

public

No filtering is applied. The full context -- page content, form values, selections, URLs -- is shared with agents exactly as captured. Use this level only for contexts you are certain contain no sensitive information, such as public documentation pages or open-source code repositories.

private

Content is shared with agents, but personally identifiable information (PII) is redacted before transmission. This is the recommended default for most browsing activity. Email addresses, patterns matching API keys, Social Security numbers, and other sensitive patterns are replaced with [redacted] markers. The structure and meaning of the content is preserved, but specific sensitive values are removed.

restricted

Only minimal metadata is shared. No page content, form values, or selection text is transmitted. Agents receive the URL, page title, tab state, and timing information, but nothing about what the user is actually reading or entering on the page. Use this for banking sites, healthcare portals, or any domain where even redacted content could be problematic.

filtered

Custom filtering rules are applied. This level uses your configured PrivacyFilterConfig to determine what gets through. You can define custom regex patterns for redaction, add custom restricted domains, and control whether URLs and emails are preserved or stripped.

The Filtering Pipeline

Periscope applies privacy filtering at two separate points:

Loading diagram…

Layer 1: Extension-Side Filtering

The first layer runs inside the browser extension, before any data leaves the user's machine. This is the most important layer because it ensures sensitive data never hits the network.

Extension-side filtering:

  • Redacts content matching sensitive patterns (passwords, API keys, tokens, SSNs)
  • Blocks contexts from restricted domains (localhost, 127.0.0.1)
  • Sanitizes form field values for sensitive field types
  • Applies custom patterns from the user's privacy configuration
  • Determines the effective privacy level for the context

Layer 2: Service-Side Filtering

The second layer runs on the Periscope service, before contexts are stored or served to agents. This layer exists as a safety net -- if the extension has a bug, or if the user's privacy configuration is misconfigured, the server applies its own filtering rules.

Service-side filtering:

  • Re-validates the privacy level
  • Applies server-configured restricted domains (can be stricter than client config)
  • Runs PII detection patterns a second time
  • Enforces organizational privacy policies (if configured)

Privacy Filter Configuration

The PrivacyFilterConfig type controls filtering behavior:

typescript
interface PrivacyFilterConfig {
  readonly contentFiltering: boolean; // Enable/disable content filtering
  readonly allowSensitiveData: boolean; // Allow sensitive data through
  readonly allowRestrictedDomains: boolean; // Allow localhost and restricted domains
  readonly customPatterns?: RegExp[]; // Additional redaction patterns
  readonly customRestrictedDomains?: string[]; // Additional blocked domains
  readonly preserveUrls: boolean; // Keep URLs in content (vs. redacting)
  readonly preserveEmails: boolean; // Keep email addresses (vs. redacting)
}

The default configuration is conservative:

typescript
const DEFAULT_PRIVACY_CONFIG: PrivacyFilterConfig = {
  contentFiltering: true,
  allowSensitiveData: false,
  allowRestrictedDomains: false,
  preserveUrls: false,
  preserveEmails: false,
  customPatterns: [],
  customRestrictedDomains: [],
};

With defaults applied, content filtering is active, sensitive data is blocked, restricted domains are blocked, and both URLs and emails in content are redacted. To relax any of these, you must explicitly set the corresponding field.

PII Redaction Patterns

Periscope uses regex patterns to detect and redact sensitive content. The default patterns match:

PatternWhat It CatchesExample Match
/password/iPassword fields and mentions"password: hunter2"
/secret/iSecret keys and values"client_secret=abc123"
/api[_-]?key/iAPI key references"apiKey: sk-abc123..."
/token/iAuthentication tokens"Bearer token: eyJ..."
/ssn/iSocial Security Number references"SSN: 123-45-6789"
Email patternEmail addresses"user@example.com"

When a pattern matches, the matched text is replaced with [redacted]. You can add custom patterns via the customPatterns field in PrivacyFilterConfig:

typescript
const config: PrivacyFilterConfig = {
  ...DEFAULT_PRIVACY_CONFIG,
  customPatterns: [
    /credit[_-]?card/i,
    /\b\d{4}[- ]?\d{4}[- ]?\d{4}[- ]?\d{4}\b/, // Credit card numbers
    /internal[_-]?id/i,
  ],
};

Restricted Domain Blocking

Certain domains are blocked by default to prevent capturing sensitive local development or internal services:

  • lattice
  • 127.0.0.1

When a context's URL matches a restricted domain, the entire context is rejected -- not redacted, but dropped entirely. The applyPrivacyFilter function returns a failure result with a "restricted_domain" violation.

You can add additional restricted domains:

typescript
const config: PrivacyFilterConfig = {
  ...DEFAULT_PRIVACY_CONFIG,
  customRestrictedDomains: [
    "internal.company.com",
    "staging.company.com",
    "admin.company.com",
  ],
};

To allow all domains (including localhost), set allowRestrictedDomains: true. This is appropriate only for development environments where you explicitly want to capture local development activity.

Form Data Filtering

Form fields receive special treatment. The filterFormData function iterates over each field in the form and applies content filtering to field values:

typescript
// Input form data
{
  formId: "login-form",
  formAction: "https://example.com/login",
  fields: [
    { name: "email", type: "email", value: "user@example.com", required: true },
    { name: "password", type: "password", value: "hunter2", required: true },
    { name: "remember", type: "checkbox", value: "true", required: false }
  ]
}

// After filtering (with default config)
{
  formId: "login-form",
  formAction: "https://example.com/login",
  fields: [
    { name: "email", type: "email", value: "[redacted]", required: true },
    { name: "password", type: "password", value: "[redacted]", required: true },
    { name: "remember", type: "checkbox", value: "true", required: false }
  ]
}

The field names, types, and structure are preserved. Only the values are filtered. This means agents can still understand the form's purpose and structure (login form with email, password, and remember-me fields) without seeing the actual credentials.

The Privacy Level Determination Function

The determinePrivacyLevel function resolves the effective privacy level for a context:

typescript
function determinePrivacyLevel(
  context: BrowserContext,
  config: PrivacyFilterConfig,
): PrivacyLevel {
  if (!config.allowSensitiveData) {
    return "restricted";
  }
  return context.privacyLevel;
}

This function is intentionally simple. If sensitive data is not allowed (the default), every context is treated as restricted regardless of what the extension originally tagged it as. Only when allowSensitiveData is explicitly set to true does the context's own privacy level take effect.

This design means that the most protective setting wins by default. A misconfigured extension that tags everything as public will still have its contexts treated as restricted by the server if the server's allowSensitiveData is false.

Privacy-Safe Logging

When you need to log context information for debugging or monitoring, use createPrivacySafeLogEntry to strip content while preserving structural metadata:

typescript
import { createPrivacySafeLogEntry } from "@lovelace-ai/periscope-core";

const logEntry = createPrivacySafeLogEntry(context);
// Result:
// {
//   id: "ctx_abc123",
//   type: "page",
//   tabId: "tab_def456",
//   userId: "user_789",
//   capturedAt: "2024-01-15T14:32:18.000Z",
//   privacyLevel: "private",
//   hasContent: true,
//   hasSelection: false,
//   hasFormData: false,
//   pageTitle: "Example Page"
// }

The log entry preserves enough information to debug issues (which context, which tab, which user, what type, whether content/selection/form data was present) without including any actual content. The pageTitle is included because it is metadata, not user-entered content, but even this could be omitted in stricter configurations.

The Full Pipeline

Putting it all together, here is the complete filtering pipeline for a single context:

1. Extension captures BrowserContext from DOM
2. Extension calls applyPrivacyFilter(context, extensionConfig)
   a. Check if URL matches restricted domain → reject if so
   b. Filter content.text through filterSensitiveContent()
   c. Filter formData fields through filterFormData()
   d. Determine effective privacyLevel via determinePrivacyLevel()
   e. Return filtered BrowserContext
3. Extension sends filtered context to service via sync protocol
4. Service validates incoming context against Zod schemas
5. Service calls applyPrivacyFilter(context, serverConfig)
   a. Same steps as extension-side, with server's config
   b. Server config may be more restrictive
6. Service stores filtered context
7. Agent queries context via REST or receives via WebSocket
   → Agent only ever sees double-filtered data

Best Practices

Default to restrictive settings. Start with the default PrivacyFilterConfig and only relax settings when you have a specific reason. It is easier to allow more data later than to retroactively remove sensitive data that has already been stored and served.

Use restricted domains for internal sites. Add your company's internal domains to customRestrictedDomains to prevent any internal page content from being captured.

Test with the filtered privacy level. Use the filtered level with custom patterns to verify that your specific redaction needs are met before deploying to users.

Do not rely solely on client-side filtering. The extension can be modified or have bugs. Server-side filtering is the authoritative layer.

Next Steps