# Security and privacy

> What the public and admin interfaces can do, how SSO, agent service tokens, the audit log and GitHub access protect them, and what happens to visitor data.

## Two kinds of access

**Public** — anyone, no credentials. The REST API (`/api/v1/*`), the public MCP server (`/mcp`), the
CLI's public commands and the static files are **read-only**. They expose only the published corpus,
which is public by design. `ask_jenks` and the website chat call a language model, so they are
rate-limited per IP address to stop abuse.

**Admin** — Jenks, and the AI agents he gives a token to. `/api/admin/*`, `/mcp/admin`,
`jenks admin …` and the console can validate, write and delete content files, upload and delete media
files, and promote `dev` to `main`. That is real power, so there are three layers.

## Sign-in for people: SSO

The console is at `https://admin.jenksguo.xyz/admin` (and `https://admin.dev.jenksguo.xyz/admin` for
the dev site). **Cloudflare Access** stands in front of those hosts: nobody reaches the console until
they sign in with an allowed Google account. `/admin` on every other address redirects there.

The Worker does not simply trust Cloudflare's headers. On every admin request it checks the signed
Access token itself — the signature against the Access team's public keys, the application's audience
tag, the issuer and the expiry — and the email must be on the admin list.

## Service tokens for agents

An AI agent never uses Jenks's sign-in. Jenks creates a **service token** for each agent in the
console (**Access → New agent token**):

- **Named** — "Claude Code on MacBook", so the audit log says which agent did what.
- **Scoped** — `content` (content, translations and site copy), `media` (the media library),
  `publish` (write straight to production and promote `dev` → `main`). Every token can read.
  Without `publish`, an agent saves to `dev` and Jenks promotes the change.
- **Expiring** — 30 to 365 days (90 by default).
- **Revocable** — one click, effective on the next request.
- **Tied to SSO** — only a person signed in through Access can create, list or revoke tokens; an
  agent token cannot make another token. A token also stops working if the person who made it is no
  longer an admin.
- **Stored as a hash** — the token is shown once, when it is created; the database keeps only its
  SHA-256 hash.

Agents send the token as `Authorization: Bearer jgx_agt_…` to `/api/admin/*` or `/mcp/admin` on
`https://jenksguo.pages.dev` (Access would stop a program on the admin host). Browsers do not send
such a header on their own, so a malicious page cannot borrow it.

## The audit log

Every admin write — and every refused attempt — is recorded: who (an email or `agent:<name>`), which
tool, which files, branch, host, and the result or error. Token creation and revocation are logged
too. File contents are never logged. Jenks reads the log in the console (**Access**); an agent can
read only its own entries (`admin_audit_log`). Git history remains the record of what changed in
the content itself.

## The break-glass admin token

The original admin token still works, for emergencies, but it cannot create agent tokens. It lives
in the macOS Keychain as `agent-env:JENKSGUO_ADMIN_TOKEN` and as an encrypted Worker secret, and is
compared in constant time. **Rotate it** if it may have leaked: store a new value in the Keychain and
the Worker secret, and the old one stops working at once. For local development only (`wrangler
dev` on localhost) the console can sign in with it: the token is exchanged once for an `HttpOnly`
session cookie.

## What the admin API can touch

Writes are limited to an allowlist of content paths — the corpus entries, lenses, skills, core
documents, the translation glossary and these docs. It cannot change code, workflows, secrets or
anything outside `content/`. Every write is validated against the content schema first.

## Media files

Images, videos and PDFs uploaded through the admin interfaces are stored in a Cloudflare R2 bucket
and served from `/media/…`. They are **public by address from the moment they are uploaded**, so only
material meant for every visitor belongs there.

- The file type is detected from the bytes, not the name, and only web image, video and PDF formats
  are accepted (25 MB each).
- SVG images are refused if they contain scripts or event handlers, and every SVG is served inside a
  content-security sandbox, so even a crafted SVG opened directly cannot run code on this site.
- Unlike content, media is not in git: deletion is permanent. Deletes are refused while published
  content still uses the file.

## GitHub access

Admin writes become commits through the GitHub API using a fine-grained personal access token scoped
to the single repository, with only the permissions publishing needs. It is stored as an encrypted
Worker secret. Because every change is a commit:

- **git history records every change** — what changed, when, and why (the commit message; the
  commit trailer names the agent),
- **undo is always possible** — any earlier version of any file can be restored.

Risky changes can be staged on the `dev` branch and checked on `https://dev.jenksguo.xyz` before
anything reaches the live site.

## Visitor privacy

- No advertising trackers and no marketing cookies.
- The chat transcript is kept only in the visitor's browser (`sessionStorage`) and cleared when the
  tab closes. The site does not store chat transcripts.
- Chat messages are sent through the Cloudflare Worker to **OpenRouter**, which routes them to a
  third-party language model. Visitors are asked not to share sensitive personal information.
- Voice is off until the visitor taps the mic, and it switches itself off after ten turns or when
  it hears no real conversation. Voice input uses the browser's own speech recognition where available;
  otherwise audio is sent to OpenRouter for transcription. Spoken replies are generated by Cloudflare
  Workers AI (Deepgram Aura-2, English) or OpenRouter (other languages), with the device's speech
  synthesis as a fallback. No recordings or generated audio are stored.
- Cloudflare keeps standard request logs for security and rate limiting.

## Content privacy

Everything under `content/` is public. Jenks's résumés contained phone numbers, a home address and
referee contacts; none of those are in the corpus, and they must never be added. Facts about other
people are limited to what is needed to describe Jenks's roles.

## Reporting a problem

Email jenksguo@gmail.com with details. Please do not test the admin interface without permission.
