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:
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:
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:
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:
| Pattern | What It Catches | Example Match |
|---|---|---|
/password/i | Password fields and mentions | "password: hunter2" |
/secret/i | Secret keys and values | "client_secret=abc123" |
/api[_-]?key/i | API key references | "apiKey: sk-abc123..." |
/token/i | Authentication tokens | "Bearer token: eyJ..." |
/ssn/i | Social Security Number references | "SSN: 123-45-6789" |
| Email pattern | Email 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:
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:
lattice127.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:
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:
// 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:
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:
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
- Browser Context Model -- The data structure that carries privacy levels and filtered content
- Sync Protocol -- How filtered contexts are transmitted to the service
- Real-Time Streaming -- How filtered events reach agents in real time