docs: bring the live docs back in line with the code

First pass of the documentation audit. Every doc was read against what the code actually
does now; this commit fixes the ones worth keeping and deletes the ones that were only
describing a past.

Corrected:
- CLAUDE.md — said seven WebSocket providers (there are eight, and terminal/vault are byte
  relays now, not translating bridges), listed channels/ as "Telegram / WhatsApp / Discord
  bridges" (they are gone; what remains is how /chat drives an agent turn), missed
  officer-wallet in the PM2 list and notify/ in the layout, and described the per-account
  email SQLite stores without saying they are the sidecar's and that nothing in the platform
  opens them. Further Reading pointed at four files that no longer exist and missed the four
  newest.
- docs/working-on-officer.md — PM2 list was four sidecars short, and it still explained the
  officer-claude rename as news. Replaced with the thing a reader actually needs: which
  process to restart for which change, and why restarting officer no longer costs you a
  terminal or an agent session.
- TODO.md — the "dead username plumbing" item was mostly resolved by deleting the channels,
  and two email items pointed at api/email/email-db.ts, which is sidecar/email/store.ts now.
- AGENTS.md — trailing paragraph listed the design notes being deleted here.
- MUSIC_API.md — playlists were entirely undocumented: seven endpoints the phone app has no
  reference for. Added from the sidecar's own contract.
- docs/jobs-unification.md — phases 1-3 shipped, so it now says so at the top. Phase 4 (push
  notifications) is the only reason the file still exists, and email sync is explicitly no
  longer part of it.

Deleted, all superseded rather than merely old:
- PHONE_APP.md — a February plan for apps that now exist, with their own repo and README.
- MARKETING_WEBSITE.md — a plan for a site this repo does not contain.
- SECURITY_AUDIT.md + SECURITY_FIXES.md — a February audit of a codebase since restructured;
  it still cites queue/handlers, which is now empty.
- docs/DOCKERIZATION_PLAN.md — cites pty-sidecar, whatsapp and projects, all deleted.
- SETUP_GUIDE.md — documents systemd units and setup scripts replaced by PM2 and `bun setup`.
  Not harmless: /etc/systemd/system/officer-pty-sidecar.service is still enabled on this host,
  pointing at a `monorepo/` directory that no longer exists, and has been failing to start
  ever since. That guide is how it got there.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-31 16:11:29 +00:00
co-authored by Claude Opus 5
parent 066c118723
commit 04c0d89057
12 changed files with 56 additions and 3315 deletions
+1 -3
View File
@@ -27,6 +27,4 @@ and no restart.
- `CONVENTIONS.md` — component organisation and React patterns, with rationale - `CONVENTIONS.md` — component organisation and React patterns, with rationale
- `TODO.md` — current direction; takes precedence where it disagrees with anything else - `TODO.md` — current direction; takes precedence where it disagrees with anything else
The other Markdown files in this repo root (`OFFICERDEV_*.md`, `MARKETING_WEBSITE.md`, Treat the code as the source of truth where anything disagrees with it.
`PHONE_APP.md`, `SECURITY_AUDIT.md`, `SETUP_*.md`, …) are older design notes and have not been kept
current. Treat the code as the source of truth.
+16 -10
View File
@@ -3,7 +3,7 @@
## Project Overview ## Project Overview
Officer is a self-hosted personal platform for one person: the server owner. It bundles an AI agent, Officer is a self-hosted personal platform for one person: the server owner. It bundles an AI agent,
a terminal, a file browser, a code editor, email, chat channels, a remote desktop and customisable a terminal, a file browser, a code editor, email, a bitcoin wallet, a remote desktop and customisable
dashboards behind a single web app. dashboards behind a single web app.
**Single-user is a hard invariant, not a stage.** There is exactly one account, created once by **Single-user is a hard invariant, not a stage.** There is exactly one account, created once by
@@ -17,15 +17,16 @@ One Bun process (`src/server.tsx`) serves everything:
- the React SPA, via Bun's HTML import of `src/apps/officer-web/index.html` (HMR in dev) - the React SPA, via Bun's HTML import of `src/apps/officer-web/index.html` (HMR in dev)
- the REST API, a Hono app mounted at `/api` (`src/servers/hono.ts`) - the REST API, a Hono app mounted at `/api` (`src/servers/hono.ts`)
- seven WebSocket providers — terminal, chat, task-runner, pipeline, cliamp, cliamp-audio, - eight WebSocket providers — terminal, chat, task-runner, pipeline, cliamp, cliamp-audio, desktop,
desktop — plus the vault notifications hub and a sidecar registration socket vault — plus a sidecar registration socket. `terminal` is a byte relay onto the pty sidecar's own
listener, not a translating bridge; `vault` is the same shape onto Vaultwarden's notifications hub.
- a browser relay on its own port (`BROWSER_RELAY_PORT`, default 18792) - a browser relay on its own port (`BROWSER_RELAY_PORT`, default 18792)
Long-running and privileged work lives in **sidecars**: separate processes that dial back in over Long-running and privileged work lives in **sidecars**: separate processes that dial back in over
`/api/sidecar/register` and are tracked in `src/servers/sidecar-registry.ts`. PM2 runs them `/api/sidecar/register` and are tracked in `src/servers/sidecar-registry.ts`. PM2 runs them
(`ecosystem.config.cjs`): `officer` (the server), `officer-anthropic-proxy`, `officer-agent`, (`ecosystem.config.cjs`): `officer` (the server), `officer-anthropic-proxy`, `officer-agent`,
`officer-opencode`, `officer-email`, `officer-pty`, `officer-vnc`, `officer-music`, `officer-vault`, `officer-opencode`, `officer-email`, `officer-pty`, `officer-vnc`, `officer-music`, `officer-vault`,
`officer-slskd`, `officer-headscale`, `officer-transmission`, `officer-invoiceshelf`. `officer-slskd`, `officer-headscale`, `officer-transmission`, `officer-invoiceshelf`, `officer-wallet`.
**`officer-anthropic-proxy` and `officer-agent` are not the same thing.** The proxy holds the Anthropic **`officer-anthropic-proxy` and `officer-agent` are not the same thing.** The proxy holds the Anthropic
credential and forwards API traffic; the agent is the process that spawns `claude`. They were one entry credential and forwards API traffic; the agent is the process that spawns `claude`. They were one entry
@@ -48,7 +49,8 @@ src/
│ ├── hono.ts # router composition; everything under /api │ ├── hono.ts # router composition; everything under /api
│ ├── _middlewares/ # auth, body parsing, origin validation, rate limiting │ ├── _middlewares/ # auth, body parsing, origin validation, rate limiting
│ ├── api/<feature>/ # one folder per feature, each exporting a router │ ├── api/<feature>/ # one folder per feature, each exporting a router
│ ├── channels/ # Telegram / WhatsApp / Discord bridges │ ├── channels/ # send-claude-code / send-opencode — how /chat drives an agent turn
│ ├── notify/ # outbound notifications (Discord webhook, env-configured)
│ ├── queue/ # background job engine │ ├── queue/ # background job engine
│ └── sidecar/ # sidecar implementations + the wire protocol │ └── sidecar/ # sidecar implementations + the wire protocol
├── databases/officer_db/ # the only database (Postgres + Drizzle) ├── databases/officer_db/ # the only database (Postgres + Drizzle)
@@ -83,8 +85,9 @@ email accounts, queue and pipeline jobs. Schema in `src/schema/`, hand-written q
**The filesystem** holds everything the agent authors. `OFFICER_ITEMS_DIR` contains one directory **The filesystem** holds everything the agent authors. `OFFICER_ITEMS_DIR` contains one directory
per item under `skills/`, `tools/`, `tasks/`, `processes/`, `extensions/` — no database rows, no per item under `skills/`, `tools/`, `tasks/`, `processes/`, `extensions/` — no database rows, no
scope tiers. `DATA_PATH/<email>/` holds the managed home, attachments and per-account email SQLite scope tiers. `DATA_PATH/<email>/` holds the managed home, attachments and the per-account email SQLite
stores. Path helpers live in `src/servers/data-path.ts`; note `getHomeDir` (the managed home under stores — those are the **email sidecar's**, and nothing in the platform opens them. Path helpers live in
`src/servers/data-path.ts`; note `getHomeDir` (the managed home under
`DATA_PATH`) versus `getOwnerHomeDir` (the owner's real login home when `HOME_DIR` is set, which is `DATA_PATH`) versus `getOwnerHomeDir` (the owner's real login home when `HOME_DIR` is set, which is
where terminals, chats and task runs actually execute). where terminals, chats and task runs actually execute).
@@ -245,12 +248,15 @@ link-focusable). Half the app still does this; none of the new code should.
## Further Reading ## Further Reading
- `CONVENTIONS.md` — component organisation, state management, React patterns, with rationale
- `docs/navigation-audit.md`**authoritative** on routing/navigation: the opaque-click anti-pattern, - `docs/navigation-audit.md`**authoritative** on routing/navigation: the opaque-click anti-pattern,
a severity-ranked findings table, the channel-selection map and the four-phase plan a severity-ranked findings table, the channel-selection map and the four-phase plan
- `docs/sidecar-topology.md` — where the sidecar architecture is going, and what was considered and dropped
- `docs/working-on-officer.md` — how to run, restart and check your work on this machine
- `docs/wallet-key-custody.md` — what the platform can and cannot see of the wallet
- `TODO.md` — current direction and deferred work; **takes precedence over this file where they disagree** - `TODO.md` — current direction and deferred work; **takes precedence over this file where they disagree**
- `src/apps/CLAUDE.md` — shared frontend patterns - `src/apps/CLAUDE.md` — shared frontend patterns
- `src/databases/CLAUDE.md` — database patterns - `src/databases/CLAUDE.md` — database patterns
- `CONVENTIONS.md` — component organisation, state management and React patterns, with rationale;
`src/workspaces/officerdev/APP_CONVENTIONS.md` and `HOOK_CONVENTIONS.md` for panel apps and hooks
The other markdown files in the repo root (`OFFICERDEV_*.md`, `MARKETING_WEBSITE.md`, `PHONE_APP.md`, Treat the code as the source of truth where anything here disagrees with it.
`SECURITY_AUDIT.md`, `SETUP_*.md`, …) are older design notes. Treat the code as the source of truth.
-716
View File
@@ -1,716 +0,0 @@
# Officer.dev Marketing Website Plan
> Written by Claude for Claude. This document covers the full marketing website — structure, copy, visuals, and demo video plans. When resuming, read this first and walk the user through the strategy before building.
---
## Table of Contents
1. [Positioning & Messaging Strategy](#1-positioning--messaging-strategy)
2. [Site Map](#2-site-map)
3. [Page-by-Page Breakdown](#3-page-by-page-breakdown)
4. [Copy Bank](#4-copy-bank)
5. [Visual Identity for Marketing](#5-visual-identity-for-marketing)
6. [Demo Videos Plan](#6-demo-videos-plan)
7. [Technical Implementation](#7-technical-implementation)
8. [Launch Considerations](#8-launch-considerations)
---
## 1. Positioning & Messaging Strategy
### The Core Idea
Officer is not a developer tool. It's not an AI chatbot wrapper. It's an **operating system** — a central place where AI does your repetitive work so you can do what matters.
### Positioning Triangle
```
OFFICER
/ \
Easier More Powerful
than than
| |
OpenClaw Claude Work
(open source (polished but
but complex) locked down)
```
**vs. OpenClaw/Open Source AI tools**: "You shouldn't need a CS degree to use AI. Officer gives you the same power — with a UI your team actually wants to use."
**vs. Claude Work/Enterprise AI**: "Enterprise AI is a walled garden. Officer is open source, self-hosted, and yours. No data leaves your servers unless you say so."
**vs. Building it yourself**: "You could stitch together 15 tools and spend months on glue code. Or you could install Officer in five minutes."
### Messaging Pillars
| Pillar | For Individuals | For Companies |
|--------|----------------|---------------|
| **Automation** | "Automate your life. Have more time to live it." | "Every minute your team spends on repetitive work is a minute wasted." |
| **AI-Native** | "An AI that doesn't just chat — it acts." | "Give every employee an AI-powered workspace." |
| **Self-Hosted** | "Your data, your server, your rules." | "Enterprise-grade AI without enterprise-grade surveillance." |
| **Open Source** | "Built in the open. Extend it however you want." | "No vendor lock-in. No surprise pricing changes." |
### Tagline Options
**Primary (hero):**
> **Your AI-powered operating system — everywhere.**
**Alternatives to test:**
> **Stop managing. Start living.**
>
> **The last tool you'll need.**
>
> **One platform. Everything automated.**
**For the business/enterprise angle:**
> **What if your entire company ran itself?**
>
> **The company intranet that actually works.**
>
> **Fortunes are lost to repetitive work every day. Not anymore.**
### Tone of Voice
- **Confident but not arrogant** — we know what we are, we don't trash competitors
- **Clear and direct** — no jargon, no buzzword soup
- **Warm** — the duck mascot sets the tone. Serious product, friendly personality
- **Action-oriented** — every section should make you want to try it
---
## 2. Site Map
```
officer.dev/
├── / (Landing / Home)
├── /features
│ ├── /features/ai-assistant
│ ├── /features/file-management
│ ├── /features/automation
│ └── /features/workspaces
├── /pricing
├── /docs (external link to docs site)
├── /marketplace (coming soon / waitlist)
├── /enterprise
├── /open-source
├── /blog (future)
└── /about
```
---
## 3. Page-by-Page Breakdown
### 3.1 Landing Page (Home)
The landing page is the entire pitch in one scroll. It must convert three different audiences: individual users, small teams, and enterprises.
#### Section 1: Hero
```
┌──────────────────────────────────────────────────┐
│ │
│ [Nav: Logo | Features | Pricing | Docs | GitHub]│
│ [ Star on GitHub | Login]│
│ │
│ Your AI-powered operating │
│ system — everywhere. │
│ │
│ Open source. Self-hosted. Free forever. │
│ │
│ [ Get Started — Free ] [ Watch Demo ▶ ] │
│ │
│ 🦆 (animated duck walks across) │
│ │
│ ┌──────────────────────────────────────────┐ │
│ │ │ │
│ │ (Hero product screenshot/video) │ │
│ │ Dashboard with widgets + chat open │ │
│ │ │ │
│ └──────────────────────────────────────────┘ │
│ │
│ Trusted by developers at [logos if available] │
│ │
└──────────────────────────────────────────────────┘
```
**Copy:**
- Headline: "Your AI-powered operating system — everywhere."
- Subheadline: "Open source. Self-hosted. Free forever."
- CTA Primary: "Get Started — Free"
- CTA Secondary: "Watch Demo"
- Background: landscape1.png with gradient overlay
**Behavior:**
- Duck walks across the hero from right to left (CSS/JS animation, lightweight — not the 3D model)
- Product screenshot below the fold, slightly overlapping
- Subtle parallax on scroll
- GitHub stars badge updates live
#### Section 2: The Problem
```
┌──────────────────────────────────────────────────┐
│ │
│ Every day, you... │
│ │
│ ┌────────┐ ┌────────┐ ┌────────┐ │
│ │ Rename │ │ Organize│ │ Copy │ │
│ │ 47 │ │ files │ │ data │ │
│ │ files │ │ into │ │ between│ │
│ │ │ │ folders │ │ apps │ │
│ └────────┘ └────────┘ └────────┘ │
│ │
│ ┌────────┐ ┌────────┐ ┌────────┐ │
│ │ Format │ │ Convert │ │ Search │ │
│ │ reports│ │ formats │ │ through│ │
│ │ │ │ │ │ docs │ │
│ └────────┘ └────────┘ └────────┘ │
│ │
│ This isn't work. It's busywork. │
│ And it costs more than you think. │
│ │
│ [$X,XXX per employee per year wasted] │
│ │
└──────────────────────────────────────────────────┘
```
**Copy:**
- "Every day, you rename files. Organize folders. Copy data between apps. Format reports. Convert formats. Search through documents."
- "This isn't work. It's busywork."
- "And it's costing you more than you think."
- Animated counter or stat: estimated cost of repetitive work per employee/year
#### Section 3: The Solution (Feature Overview)
```
┌──────────────────────────────────────────────────┐
│ │
│ Meet Officer. │
│ │
│ The platform that combines AI, file management, │
│ automation, and workspaces into one self-hosted │
│ operating system. │
│ │
│ ┌─────────────────────────────────────────┐ │
│ │ │ │
│ │ (Animated product showcase) │ │
│ │ Cycling through: Chat → Files → │ │
│ │ Automation → Dashboard │ │
│ │ │ │
│ └─────────────────────────────────────────┘ │
│ │
│ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ │
│ │ 🤖 │ │ 📁 │ │ ⚡ │ │ 🏠 │ │
│ │ AI │ │Files │ │Auto- │ │Work- │ │
│ │Assist│ │ │ │mate │ │space │ │
│ └──────┘ └──────┘ └──────┘ └──────┘ │
│ │
└──────────────────────────────────────────────────┘
```
**Four feature cards:**
1. **AI Assistant** — "Not just chat. An AI that reads your files, runs your tasks, and gets things done."
2. **File Management** — "Your entire digital life in one place. Browse, preview, edit, convert, transcribe."
3. **Automation** — "Build workflows that run themselves. Tasks, cron jobs, pipelines — without writing code."
4. **Workspaces** — "Your desk, digitized. Combine any tool into a custom layout that fits how you work."
Each card links to its feature detail page.
#### Section 4: For You / For Your Team (Split Section)
```
┌──────────────────────────────────────────────────┐
│ │
│ ┌─────────────────┐ ┌─────────────────────┐ │
│ │ │ │ │ │
│ │ FOR YOU │ │ FOR YOUR COMPANY │ │
│ │ │ │ │ │
│ │ "Automate your │ │ "What if your │ │
│ │ life. Have more │ │ entire team had an │ │
│ │ time to live │ │ AI-powered intranet │ │
│ │ it." │ │ that actually │ │
│ │ │ │ worked?" │ │
│ │ • Personal AI │ │ │ │
│ │ assistant │ │ • Role-based access │ │
│ │ • File organiz- │ │ • Shared workspaces │ │
│ │ ation on │ │ • Company-wide │ │
│ │ autopilot │ │ automation │ │
│ │ • Smart dash- │ │ • Self-hosted: │ │
│ │ board with │ │ your data stays │ │
│ │ your goals │ │ on your servers │ │
│ │ • Voice notes, │ │ • Plugin ecosystem │ │
│ │ OCR, TTS │ │ for custom needs │ │
│ │ │ │ │ │
│ │ [Try Free →] │ │ [Enterprise →] │ │
│ │ │ │ │ │
│ └─────────────────┘ └─────────────────────┘ │
│ │
└──────────────────────────────────────────────────┘
```
#### Section 5: How It Works
```
┌──────────────────────────────────────────────────┐
│ │
│ Up and running in 5 minutes. │
│ │
│ 1. Install │
│ ┌──────────────────────────────────┐ │
│ │ $ curl -fsSL install.officer.dev │ │
│ │ | sh │ │
│ └──────────────────────────────────┘ │
│ One command. That's it. │
│ │
│ 2. Open your browser │
│ Navigate to localhost:5000. │
│ Create your admin account. │
│ │
│ 3. Start automating │
│ Your AI assistant is ready. │
│ Your files are connected. │
│ Your first automation is one click away. │
│ │
│ [Get Started — Free] │
│ │
└──────────────────────────────────────────────────┘
```
#### Section 6: Open Source & Community
```
┌──────────────────────────────────────────────────┐
│ │
│ Built in the open. Extended by everyone. │
│ │
│ Officer is open source under [LICENSE]. │
│ Read every line. Modify anything. │
│ Self-host for free, forever. │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ ⭐ │ │ 🔌 │ │ 🎨 │ │
│ │ [count] │ │ Plugins │ │ Themes │ │
│ │ GitHub │ │ Build & │ │ Make it │ │
│ │ Stars │ │ share │ │ yours │ │
│ └──────────┘ └──────────┘ └──────────┘ │
│ │
│ Marketplace coming soon. │
│ Build plugins. Sell themes. Earn money. │
│ │
│ [Star on GitHub ⭐] [Join the community] │
│ │
└──────────────────────────────────────────────────┘
```
#### Section 7: Pricing Preview
```
┌──────────────────────────────────────────────────┐
│ │
│ Simple pricing. No surprises. │
│ │
│ ┌──────────┐ ┌───────────┐ ┌──────────┐ │
│ │ Self- │ │ Cloud │ │ Enter- │ │
│ │ Hosted │ │ │ │ prise │ │
│ │ │ │ │ │ │ │
│ │ FREE │ │ $XX/mo │ │ Custom │ │
│ │ forever │ │ per user │ │ │ │
│ │ │ │ │ │ │ │
│ │ Full │ │ Everything│ │ SLA, │ │
│ │ features │ │ hosted & │ │ support, │ │
│ │ Your │ │ managed │ │ custom │ │
│ │ servers │ │ for you │ │ features │ │
│ │ │ │ │ │ │ │
│ │ [Install]│ │ [Start] │ │ [Talk] │ │
│ └──────────┘ └───────────┘ └──────────┘ │
│ │
│ All plans include all features. │
│ The only difference is who runs the server. │
│ │
└──────────────────────────────────────────────────┘
```
**Key messaging**: "All plans include all features. The only difference is who runs the server." This is a powerful differentiator. No feature gating.
#### Section 8: CTA / Footer
```
┌──────────────────────────────────────────────────┐
│ │
│ Ready to stop wasting time? │
│ │
│ [Get Started — Free] [Book a Demo] │
│ │
│ ───────────────────────────────────────────── │
│ │
│ officer.dev │
│ Product: Features | Pricing | Docs │
│ Community: GitHub | Discord | Blog │
│ Company: About | Enterprise | Contact │
│ │
│ © 2026 Officer.dev │
│ │
└──────────────────────────────────────────────────┘
```
---
### 3.2 Features Page (`/features`)
Overview page with anchor-linked sections for each feature area. Each section has:
- Feature title & one-line pitch
- 3-4 bullet capabilities
- Embedded demo video or animated screenshot
- CTA to try it
**Feature areas:**
#### AI Assistant (`/features/ai-assistant`)
- Multi-provider (Claude, OpenCode, Pi-Mono)
- File-aware — reads, edits, and creates files
- Task-aware — runs automation on your behalf
- Voice input, image analysis, code understanding
- Session persistence across devices
**Demo video**: Show a conversation where the user asks "organize my downloads folder by file type" and the AI does it, showing the file browser updating in real-time.
#### File Management (`/features/file-management`)
- Browse, upload, download, search
- Built-in viewers: text, images, PDFs, audio, video
- AI-powered: OCR, text-to-speech, transcription, audio extraction
- Video transcoding for any format
- Archive extraction (zip, tar, 7z, rar)
- Git clone, yt-dlp download integration
**Demo video**: Upload a photo of a receipt → OCR extracts the text → AI categorizes the expense. All in Officer.
#### Automation (`/features/automation`)
- Tasks triggered by file type, schedule, or manually
- Workflows chaining multiple steps
- Cron jobs for recurring work
- Background processes
- Skills system — teach AI new capabilities
**Demo video**: Show setting up a task that compresses images when they're uploaded to a specific folder.
#### Workspaces (`/features/workspaces`)
- Multi-panel layouts
- Combine chat + files + any tool
- Save and switch between workspace configurations
- Create workspace templates for team
**Demo video**: Building a "Content Creation" workspace with file browser on the left, AI chat in the middle, and preview on the right.
---
### 3.3 Pricing Page (`/pricing`)
Three tiers, feature-complete across all. The difference is hosting and support.
**Self-Hosted (Free)**
- All features, unlimited users
- You host, you manage
- Community support (GitHub, Discord)
- "Perfect for developers, homelab enthusiasts, and teams who want full control."
**Cloud (Subscription — $XX/user/month)**
- All features, unlimited
- We host and manage everything
- Automatic updates, backups, SSL
- Email support
- "For teams who want Officer without the DevOps."
**Enterprise (Custom)**
- Everything in Cloud
- SLA with guaranteed uptime
- Priority support with dedicated account manager
- Custom feature development
- SSO/SAML integration
- Audit logging
- On-premises deployment assistance
- "For organizations that need enterprise-grade guarantees."
**Below the pricing cards:**
- FAQ section (Is it really free? Can I migrate between plans? What about data portability?)
- "Need something custom?" → Contact form
- "Building plugins?" → Marketplace waitlist signup
---
### 3.4 Enterprise Page (`/enterprise`)
**Audience**: IT decision-makers, CTOs, operations managers.
**Tone**: Professional, emphasizing reliability, security, and ROI.
**Sections:**
1. **Hero**: "The company intranet that works for you — literally."
2. **Problem**: "Your team uses 12 different tools. None of them talk to each other. Officer replaces the glue."
3. **Security**: "Self-hosted means your data never leaves your servers. Full audit logging. Role-based access. SOC 2 compliance roadmap."
4. **ROI Calculator**: Interactive widget — input team size, estimated hours on repetitive tasks → shows annual savings
5. **Consultancy**: "Our team builds custom integrations, plugins, and workflows tailored to your operations."
6. **Case studies**: (placeholder for future customer stories)
7. **CTA**: "Book a Demo" → Calendly or contact form
---
### 3.5 Open Source Page (`/open-source`)
**Audience**: Developers, contributors, plugin builders.
**Sections:**
1. **Hero**: "Built in the open. Extended by everyone."
2. **Why Open Source**: Philosophy statement — transparency, community ownership, no vendor lock-in
3. **Contributing**: Quick guide to getting started, link to CONTRIBUTING.md
4. **Plugin Development**: Overview of the plugin system, link to docs
5. **Marketplace Preview**: "Coming soon — build and sell plugins, themes, and automations. Earn money from your work."
6. **GitHub Stats**: Stars, forks, contributors, recent commits
7. **CTA**: "Star on GitHub" / "Join Discord"
---
## 4. Copy Bank
### Headlines (A/B test candidates)
**Automation-focused:**
- "Your AI-powered operating system — everywhere."
- "Stop doing. Start automating."
- "The work that works itself."
- "Automate everything. Miss nothing."
**For individuals:**
- "Your second brain. But it actually does things."
- "AI that doesn't just answer — it acts."
- "Automate your life. Have more time to live it."
**For companies:**
- "What if your team's busywork just... disappeared?"
- "The company intranet that works for you — literally."
- "Fortunes are lost to repetitive work every day. Meet the fix."
- "Every employee deserves an AI assistant. Now they can have one."
**Open source angle:**
- "Open source. Self-hosted. No strings attached."
- "Your data. Your server. Your AI."
- "The power of enterprise AI. The freedom of open source."
### Social Proof (to collect/create)
- GitHub stars count
- "Used by X developers/companies"
- Testimonials (collect after launch)
- "Featured in [publications]" (aim for Hacker News, ProductHunt, etc.)
### Microcopy
- Install CTA: "Get Started — Free"
- Cloud CTA: "Start your trial"
- Enterprise CTA: "Book a demo"
- GitHub CTA: "Star on GitHub"
- Newsletter CTA: "Get updates"
- Marketplace waitlist: "Join the waitlist"
---
## 5. Visual Identity for Marketing
### Design System Adaptation
The product uses a duck-themed design (teal, yellow, forest green, glass-morphism with pixel grid). The marketing site should feel like the same brand but optimized for conversion:
**Colors:**
- Primary: `#F4C430` (Duck Yellow) — CTAs, highlights
- Secondary: `#0891B2` (Duck Teal) — links, accents
- Dark: `#14532D` (Duck Dark) — text, headings
- Background: Soft cream/beige (`#F5F0E8`) or white with subtle grid texture
- Dark mode: Support it, matching the product's dark theme
**Typography:**
- Headings: Bold, system-ui stack with the same skewed/3D shadow effect as the logo (but subtler for readability)
- Body: Clean sans-serif (Inter or system-ui), 18px base
- Code: Monospace for install commands and API examples
**The Duck:**
- Hero: Subtle walking animation (lightweight 2D sprite or CSS, not the 3D model — save loading time)
- Scroll companion: Small duck that appears in corners, reacts to scroll
- 404 page: Lost duck illustration
- Loading states: Duck waddle animation
**Photography/Visuals:**
- Product screenshots with the landscape backgrounds
- Minimal mockups (no fake Chrome windows — just clean screenshots with rounded corners and shadow)
- Feature diagrams: Simple, geometric, using brand colors
**Grid Pattern:**
- Carry the pixel grid texture from the product into section backgrounds
- Use it subtly — don't overwhelm the content
### Section Visual Hierarchy
| Section | Background | Text Color |
|---------|-----------|------------|
| Hero | landscape1.png with dark overlay | White |
| Problem | White/cream | Dark green |
| Solution | Subtle grid pattern | Dark green |
| For You/Company | Split — light teal / light yellow | Dark |
| How It Works | White | Dark |
| Open Source | Dark (forest green) | Yellow/white |
| Pricing | White/cream | Dark |
| CTA | Teal gradient | White/yellow |
---
## 6. Demo Videos Plan
### Product Demo Reel (60s — for hero)
A fast-paced showcase that cycles through the major features:
| Time | Scene | Description |
|------|-------|-------------|
| 0-5s | Cold open | Tagline appears over landscape. Duck walks in. |
| 5-12s | Chat | User asks AI to "summarize all PDFs in my project folder." AI responds with tool use, result appears. |
| 12-20s | Files | File browser opens. User uploads files, previews images, plays audio. Quick montage. |
| 20-30s | Automation | User creates a task. It runs automatically. Toast notification: "Task complete." |
| 30-40s | Workspace | Multi-panel workspace assembles itself — chat + files + viewer. User drags to resize. |
| 40-48s | Dashboard | Widgets dashboard: clock, weather, goals getting checked off, pomodoro timer running. |
| 48-55s | Scale | "For you. For your team. For your company." — zoom out to show multiple users. |
| 55-60s | CTA | Logo + "Open source. Self-hosted. Free forever." + URL |
### Feature-Specific Demos (30s each)
**AI Assistant Demo:**
- Show real conversation with tool use
- AI reads a file, suggests changes, user approves
- Emphasis on the AI *doing things*, not just chatting
**File Management Demo:**
- Upload → organize → preview → AI actions (OCR, transcribe)
- Show the breadth of file types supported
- yt-dlp download as a wow moment
**Automation Demo:**
- Create a task that processes files automatically
- Show it triggering on file upload
- End with "Set it up once. It runs forever."
**Workspaces Demo:**
- Start with empty screen
- Build a workspace: add chat, files, viewer
- Split panels, resize
- "Your desk. Your way."
### Recording Guidelines
- **Resolution**: 1920x1080 for landscape, 1080x1920 for portrait (social)
- **Browser**: Clean browser, no bookmarks bar, no extensions visible
- **Data**: Use realistic-looking sample data (not lorem ipsum)
- **Cursor**: Smooth, deliberate movements. No jitter.
- **Speed**: 1.5x natural speed for repetitive parts, real-time for "wow" moments
- **Music**: Upbeat instrumental for playful, ambient electronic for professional
---
## 7. Technical Implementation
### Stack Recommendation
The marketing site should be **separate from the main monorepo** — it's a public website with different deployment, SEO, and performance requirements.
**Recommended stack:**
- **Framework**: Astro (static-first, great SEO, supports React islands for interactive parts)
- **Styling**: Tailwind CSS (matches the product's design system)
- **Animations**: Framer Motion (for scroll-triggered animations)
- **Hosting**: Vercel or Cloudflare Pages (fast global CDN)
- **CMS**: Markdown files for blog posts (future)
- **Analytics**: Plausible or PostHog (privacy-friendly)
**Alternative**: If the team prefers to keep everything in the monorepo's React ecosystem, a Next.js or Vite app could work. But Astro's static output and built-in SEO features make it better for a marketing site.
### Project Location
Create at `/home/pastilhas/projects/officer.dev/website/` — separate from the monorepo but within the project directory.
### SEO Strategy
**Target keywords:**
- "self-hosted AI platform"
- "open source AI assistant"
- "AI-powered intranet"
- "personal AI operating system"
- "self-hosted automation"
- "open source alternative to [Claude Work/Notion AI/etc.]"
- "AI file management"
**Technical SEO:**
- Static HTML pages (Astro)
- Proper meta tags, OpenGraph images per page
- JSON-LD structured data
- Sitemap.xml
- Fast Core Web Vitals (landscape images need lazy loading + WebP conversion)
### Performance Considerations
- landscape1.png is 16MB — must be converted to WebP/AVIF and served responsive
- The 3D duck model (40MB total) should NOT be on the marketing site — use a lightweight 2D animation instead
- Use the rubber-duck.png (1.6MB) at reduced size for the duck character
- Lazy load everything below the fold
---
## 8. Launch Considerations
### Pre-Launch Checklist
- [ ] Marketing site live with all sections
- [ ] Product demo video recorded and embedded
- [ ] GitHub repo public with README, CONTRIBUTING.md, LICENSE
- [ ] Installation script (`install.officer.dev`) working
- [ ] Documentation site (even minimal) live
- [ ] Discord/community channel created
- [ ] Social accounts created (Twitter/X, LinkedIn)
- [ ] ProductHunt launch page drafted
- [ ] Hacker News "Show HN" post drafted
- [ ] Email waitlist for cloud/marketplace
### Launch Channels
| Channel | Content | Timing |
|---------|---------|--------|
| Hacker News | "Show HN: Officer — open-source, AI-powered OS for automating your life" | Day 1 |
| ProductHunt | Full launch with screenshots, video, maker story | Day 1 |
| Reddit | r/selfhosted, r/homelab, r/opensource, r/artificial | Day 1 |
| Twitter/X | Thread: "I built an OS that automates my life. It's open source." | Day 1 |
| LinkedIn | Professional angle: "Every company wastes $X on repetitive work" | Day 2 |
| Dev.to / Hashnode | Technical deep-dive blog post | Week 1 |
| YouTube | Full demo video (5min) | Week 1 |
### Post-Launch
- Monitor GitHub issues and community channels
- Publish weekly blog posts (features, tutorials, philosophy)
- Start marketplace waitlist campaign
- Collect testimonials from early adopters
- Begin enterprise outreach
---
## Appendix: Competitor Landscape
| Product | What it is | Officer's advantage |
|---------|-----------|-------------------|
| **Notion** | Wiki + project management | Officer has AI that acts, not just organizes. Self-hosted. |
| **Obsidian** | Markdown knowledge base | Officer is broader — files, AI, automation, not just notes. |
| **Nextcloud** | Self-hosted file sync | Officer has native AI, automation, and workspaces. |
| **n8n** | Workflow automation | Officer combines automation with AI + file management + UI. |
| **Claude Work** | Enterprise AI chat | Officer is open source, self-hosted, and more extensible. |
| **OpenClaw** | Open source AI tools | Officer is friendlier, more polished, broader scope. |
| **Home Assistant** | Home automation | Different domain, but similar self-hosted philosophy. Potential integration. |
Officer's unique position: **No one else combines AI + files + automation + workspaces in a single self-hosted platform.** That's the moat.
+18
View File
@@ -293,6 +293,24 @@ launch to offer "resume".
--- ---
### Playlists
Server-side playlists, scoped to the calling user. Items are track **keys** — the same
`<albumRel>/<file>` strings favorites uses — so a playlist survives a reindex as long as the file stays
put. `404` throughout means "not yours or not there"; the two are deliberately indistinguishable.
| method | path | body | returns |
|---|---|---|---|
| `GET` | `/api/music/playlists` | — | `[{ id, name, count, createdAt, updatedAt }]`, most recent first |
| `POST` | `/api/music/playlists` | `{ name }` | `201` with the row; `409` if the name is taken |
| `GET` | `/api/music/playlists/:id` | — | `{ id, name, items: [key], … }` |
| `PATCH` | `/api/music/playlists/:id` | `{ name }` | rename; `409` if taken |
| `DELETE` | `/api/music/playlists/:id` | — | deletes it, items cascade |
| `POST` | `/api/music/playlists/:id/items` | `{ keys: [] }` | append → `{ count }` |
| `PUT` | `/api/music/playlists/:id/items` | `{ keys: [] }` | replace the whole list → `{ count }` |
`PUT` is how you reorder or remove: send the list you want, in order. There is no per-item delete.
## Notes ## Notes
- **Covers are server-compressed** (≤600px / q5) — sync them as-is; no client-side resizing needed. - **Covers are server-compressed** (≤600px / q5) — sync them as-is; no client-side resizing needed.
-1208
View File
File diff suppressed because it is too large Load Diff
-593
View File
@@ -1,593 +0,0 @@
# Security Audit — Officer.dev Pi-Monorepo
**Date:** 2026-02-25
**Scope:** Full-stack audit of server, frontend, and infrastructure code.
---
## Executive Summary
The codebase has solid foundations (JWT + argon2 auth, WebAuthn passkeys, token blacklisting, Docker-sandboxed terminals, role-based access control). However, several critical and high-severity vulnerabilities exist — primarily around CORS misconfiguration, weak secret management, unauthenticated admin routes, XSS via `rehype-raw`, and token exposure in URLs.
**Totals:** 5 Critical, 13 High, 14 Medium, 9 Low, 5 Info
---
## CRITICAL
### C1. Weak JWT Secret — `"officer"`
**Files:** `.env` (line 2), `src/servers/jwt.ts` (lines 2, 25)
The live `.env` contains `JWT_SECRET="officer"` — a single dictionary word. The `.env.example` uses `"change-me"`. The server uses a non-null assertion (`JWT_SECRET!`) with no startup validation for length or complexity.
**Impact:** Complete authentication bypass. Anyone who knows/guesses the secret can forge JWTs for any user including Super Admin.
**Remediation:**
- Generate a cryptographically random secret: `openssl rand -base64 32`
- Add a startup guard: throw if `JWT_SECRET` is absent or shorter than 32 characters
- Update `.env.example` with a placeholder: `JWT_SECRET=<generate with: openssl rand -base64 32>`
---
### C2. Global CORS Wildcard on All API Routes
**File:** `src/servers/hono.ts` (lines 3642)
```ts
cors({ origin: '*', allowHeaders: ['Content-Type', 'Authorization'] })
```
Applied to every route. The `Authorization` header allowlist means any website can make cross-origin requests using a victim's token (obtained via XSS or other leaks).
**Impact:** Full cross-origin request forgery against all API endpoints for users tricked into visiting a malicious page.
**Remediation:** Replace `origin: '*'` with the allowlist from `origin-validation.ts` (`ALLOWED_ORIGINS`). Use a dynamic origin validation function in the CORS middleware.
---
### C3. Server-Settings Routes Entirely Unauthenticated
**Files:** `src/servers/hono.ts` (lines 4648), `src/servers/api/server-settings/server-settings.ts`
The `serverSettingsRouter` is mounted on the **public** router (outside `protectedRouter`). All sub-routes — SMTP, TTS, STT, OCR, SearXNG, Pi-Mono, Claude Code, OpenCode, applications, resources — have no auth checks. Any unauthenticated request can read settings (including masked credentials), write new settings, and trigger software installs.
**Impact:** Full server configuration takeover by any unauthenticated user.
**Remediation:** Move `serverSettingsRouter` inside `protectedRouter`. Add Super Admin role checks on all write operations. Exempt only `GET /api/server-settings/onboarding-complete` as public.
---
### C4. Unauthenticated Dev-Server WebSocket Proxy
**Files:** `src/server.tsx` (lines 114139), `src/servers/api/dev-server/router.ts` (lines 241268), `src/servers/hono.ts` (line 49)
The dev-server proxy (`/api/dev-server-proxy/*`) is mounted on the public router. WebSocket upgrades carry no authentication. The HTTP proxy has no auth gate either. Additionally, the Referer header fallback allows probing any running dev server.
**Impact:** Unauthenticated access to dev-servers spawned for any user, which may expose internal APIs, HMR endpoints, or local services.
**Remediation:** Require a valid JWT (via query param) before upgrading WebSocket connections. Apply `userMiddleware` to the HTTP proxy router.
---
### C5. AI Chat Messages Rendered with `rehype-raw` — LLM-Driven XSS
**File:** `src/workspaces/officerdev/src/apps/Chat/components/MessageBubble.tsx` (lines 132, 181)
```tsx
<ReactMarkdown rehypePlugins={[rehypeRaw]}>{assistantText}</ReactMarkdown>
```
AI-generated assistant responses containing HTML tags (`<script>`, `<iframe>`, `<img onerror=...>`) are rendered verbatim into the DOM.
**Impact:** Stored XSS in the chat interface. Prompt injection via uploaded files, tool results, or a compromised AI provider can execute arbitrary JavaScript — exfiltrating the Bearer token from localStorage.
**Remediation:** Remove `rehype-raw` from chat rendering. If basic HTML is needed, use `rehype-sanitize` with an allowlist of safe tags (no `<script>`, `<iframe>`, event handlers).
---
## HIGH
### H1. Bearer Token Stored in `localStorage`
**Files:** `src/workspaces/hooks/src/useAuth/useAuth.ts` (lines 56, 84, 99), `usePasskeys.ts` (line 44)
The primary 30-day authentication token is stored in `localStorage`, readable by any JavaScript on the page origin — including via XSS.
**Impact:** Combined with any XSS vector (C5, H5, H6), an attacker can exfiltrate the token and hijack the session.
**Remediation:** Use `HttpOnly` cookies for the Bearer token so it is inaccessible to JavaScript.
---
### H2. Bearer Token Exposed in WebSocket/Media URL Query Strings
**Files:** `src/workspaces/officerdev/src/hooks/usePiChat.ts` (line 58), `src/workspaces/officerdev/src/apps/Terminal/Terminal.tsx` (line 43), `src/workspaces/officerdev/src/apps/FileViewer/file-types.ts` (lines 144148), `src/server.tsx` (line 87), `src/servers/_middlewares/user-middleware.ts` (line 32)
The full 30-day JWT appears in URLs for WebSocket connections, raw file downloads, and media transcoding.
**Impact:** Tokens are logged by servers, proxies, CDNs; stored in browser history; sent in `Referer` headers; visible to browser extensions and analytics scripts.
**Remediation:** For WebSocket: send the token as the first message after connection. For media: use short-lived single-use tokens generated by a dedicated endpoint (e.g., 60-second TTL).
---
### H3. `rehype-raw` Used in Multiple Content-Rendering Contexts
**Files:**
- `src/apps/officer-web/Screens/Dashboard/CapabilityPage.tsx` (line 354)
- `src/apps/officer-web/Screens/Dashboard/Plans/index.tsx` (line 5)
- `src/apps/officer-web/Screens/Dashboard/Automation/CapabilityDetailView.tsx` (line 5)
- `src/apps/officer-web/Screens/Dashboard/Settings/ResourceSettings/Resources.tsx` (line 5)
- `src/workspaces/components/MarkdownEditor.tsx` (line 98)
- `src/workspaces/officerdev/src/apps/FileViewer/renderers/MarkdownRenderer.tsx` (line 43)
Markdown from user-editable files (skills, tasks, processes, arbitrary filesystem files) is rendered with `rehype-raw`, allowing embedded HTML/script injection.
**Impact:** Stored XSS. A malicious Markdown file opened in the file viewer or capability page executes arbitrary JavaScript.
**Remediation:** Add `rehype-sanitize` alongside `rehype-raw` in all locations, or remove `rehype-raw` entirely for untrusted content.
---
### H4. Email HTML Rendered with `allow-same-origin` Sandbox
**File:** `src/apps/officer-web/Screens/Dashboard/Email/EmailReader.tsx` (lines 2541)
HTML email bodies are written into an iframe via `document.write()` with `sandbox="allow-same-origin"`. While `allow-scripts` is not set (blocking script execution), CSS injection and phishing forms can still render.
**Impact:** Email phishing attacks rendered within the Officer UI. CSS-based data exfiltration possible.
**Remediation:** Change to `sandbox=""` (no attributes). Consider running email HTML through DOMPurify server-side.
---
### H5. Mass Email Template — Unsanitized `dangerouslySetInnerHTML`
**Files:** `src/workspaces/emailer/emails/MassEmail.tsx` (line 23), `src/workspaces/components/MarkdownEditor.tsx` (line 41)
```tsx
<div dangerouslySetInnerHTML={{ __html: greetingHtml + content }} />
```
User names are interpolated via `String.replace()` without HTML encoding. A user with `<script>` in their name triggers XSS in the admin's email preview.
**Impact:** Stored XSS in admin email previews.
**Remediation:** HTML-encode all interpolated user data. Use DOMPurify on the final output.
---
### H6. `applyGlobals` — Arbitrary JavaScript Injection
**File:** `src/workspaces/injector/apply-changes/apply-globals.ts` (lines 1113)
```ts
script.innerHTML = `;(function(){${globalJavascript}})();`;
document.body.appendChild(script);
```
A/B test experiment configuration injects arbitrary JavaScript into every user's browser session.
**Impact:** If a non-admin can influence the `globalJavascript` payload, it's instant code execution across the organization.
**Remediation:** Restrict write access to experiment configurations to Admin/Owner/Super Admin. Audit API endpoints that set `globalJavascript`.
---
### H7. Unsanitized URL Passed to `yt-dlp` and `git clone`
**File:** `src/servers/api/file-browser/router.ts` (lines 901944)
Both `yt-dlp` and `git clone` are invoked with user-supplied URLs without scheme validation. `git clone file:///etc/passwd` reads local files. `yt-dlp` accepts plugin URLs and SSRF targets.
**Impact:** Local file exfiltration via `file://`, SSRF to internal services, potential exploitation via git remote helpers.
**Remediation:** Validate URLs to allow only `http://` and `https://` schemes. Add `--` argument terminator and `--no-config` for yt-dlp. Add `--no-local` for git.
---
### H8. File Browser Exposes Host Filesystem to Super Admin
**File:** `src/servers/api/file-browser/router.ts` (lines 5461)
Super Admin can access `~` (host homedir) and `officer.dev` (project root), giving read/write access to `.env`, auth store files, API keys, and the entire host filesystem.
**Impact:** A compromised Super Admin account (or forged JWT — trivial with C1) has full host filesystem access.
**Remediation:** Restrict roots to `DATA_PATH` only. Remove `~` and `officer.dev` roots or restrict them to read-only with path filtering.
---
### H9. Rate Limiting Disabled Outside Production
**File:** `src/servers/_middlewares/rate-limiter.ts` (lines 2728, 4546)
All rate limiters are bypassed unless `PUBLIC_BUILD_ENV` is `production` or `staging`. The `.env` doesn't set this variable, so rate limiting is off by default.
**Impact:** Unrestricted brute-force attacks on passwords, verification codes, and passkey challenges in any deployment that doesn't explicitly set `PUBLIC_BUILD_ENV=production`.
**Remediation:** Enable rate limiting by default. Only disable when `PUBLIC_BUILD_ENV === 'test'`.
---
### H10. Rate Limiter Uses Spoofable `X-Forwarded-For`
**File:** `src/servers/_middlewares/rate-limiter.ts` (line 34)
The default key is the raw `X-Forwarded-For` header, which is client-controlled and trivially spoofed.
**Impact:** Complete rate limit bypass by setting any `X-Forwarded-For` value.
**Remediation:** Use the actual socket IP (Bun provides this) or validate `X-Forwarded-For` against a trusted proxy list.
---
### H11. Terminal Sidecar Users Get Passwordless Sudo
**File:** `src/servers/api/terminal/entrypoint.sh` (line 24)
```sh
echo "$USERNAME ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/terminal-user
```
**Impact:** Full root inside container. If Docker sandboxing is bypassed or misconfigured, this escalates to host root.
**Remediation:** Remove NOPASSWD sudo entirely or restrict to specific commands.
---
### H12. Google OAuth State Parameter Not Signed
**File:** `src/servers/api/integrations/integrations.ts` (lines 134150)
The OAuth state is just `base64(JSON)` — no HMAC. The `origin` query parameter is user-controlled and embedded in the state. The callback is a public endpoint.
**Impact:** OAuth CSRF. An attacker can craft a state parameter to bind their Google account to a victim's email.
**Remediation:** Sign the state with HMAC using `JWT_SECRET`. Derive `redirectUri` from `PUBLIC_URL`, not from the query parameter.
---
### H13. `maxRequestBodySize` Set to 50 GB
**File:** `src/server.tsx` (line 144)
**Impact:** DoS by exhausting server memory/disk with a single request.
**Remediation:** Set a reasonable limit (100 MB1 GB). Apply per-route size limits for file uploads.
---
## MEDIUM
### M1. Rate Limiter Only Counts Failed Requests
**File:** `src/servers/_middlewares/rate-limiter.ts` (lines 5967)
Only 4xx responses increment the counter. Successful auths reset the window.
**Impact:** Attackers can interleave successful/failed attempts to effectively bypass rate limits.
**Remediation:** Count all requests toward the limit, regardless of response code.
---
### M2. Password Reset Tokens Not Blacklisted After Use
**File:** `src/servers/api/auth/reset-password.ts`
The same reset link can be used multiple times within its validity window.
**Impact:** Reset link replay — an intercepted link works even after the user has already reset their password.
**Remediation:** Blacklist the token's JTI immediately after first use.
---
### M3. Password Validation Skipped in Non-Production
**File:** `src/servers/api/auth/change-password.ts` (lines 813)
```ts
if (isProduction) validatePassword(newPassword);
```
**Impact:** Weak/empty passwords accepted if `PUBLIC_BUILD_ENV` is unset in production.
**Remediation:** Always validate password complexity regardless of environment.
---
### M4. User Settings PUT — No Schema Validation or Size Limit
**File:** `src/servers/api/settings/settings.ts` (lines 4351)
The entire request body is written verbatim to the settings JSON file.
**Impact:** Disk exhaustion, injection of arbitrary keys that may affect server-side logic (e.g., `tts.voice`).
**Remediation:** Apply a size limit (64 KB) and schema validation for known fields only.
---
### M5. SSRF via Scrape Endpoint (Playwright)
**File:** `src/servers/api/scrape/scrape.ts` (lines 3145)
The scrape endpoint navigates Playwright to any user-supplied URL without validation.
**Impact:** SSRF to internal network, cloud metadata (`169.254.169.254`), and localhost services. Full HTML returned.
**Remediation:** Validate scheme (`http://`/`https://` only), resolve hostname, block private IP ranges.
---
### M6. SSRF via User-Configured OCR/TTS/STT URLs
**File:** `src/servers/api/file-browser/router.ts` (lines 509545)
External API URLs come from server configuration (writable via unauthenticated C3 routes). An attacker can point them to internal services.
**Impact:** SSRF to internal network via AI provider proxy.
**Remediation:** Fix C3 first. Then validate configured URLs don't resolve to private/loopback ranges.
---
### M7. OCR Sends Arbitrary Files to External Vision API
**File:** `src/servers/api/file-browser/router.ts` (lines 481555)
MIME type derived from extension only. No file type validation or size limit.
**Impact:** Data exfiltration of non-image files (config, keys) to third-party AI providers.
**Remediation:** Validate magic bytes, enforce max size, restrict allowed extensions.
---
### M8. `resend-verification` Leaks User Existence
**File:** `src/servers/api/auth/resend-verification.ts` (line 13)
Returns distinct responses for "user not found" vs "already verified" with no rate limiting.
**Impact:** Automated user enumeration.
**Remediation:** Return the same success response regardless. Add rate limiting.
---
### M9. API Keys + `JWT_SECRET` Leaked to AI Subprocess
**File:** `src/servers/api/pi/pi-bridge.ts` (lines 234235, 281283)
The unsandboxed path spreads `process.env` (including `JWT_SECRET`, DB credentials, SMTP passwords) into every Pi process.
**Impact:** A prompt-injected AI model can exfiltrate all server secrets.
**Remediation:** Explicitly enumerate allowed environment variables. Never pass `JWT_SECRET` or DB credentials to AI subprocesses.
---
### M10. Auth Store Has No File Locking
**File:** `src/databases/officer_db/src/store.ts`
In-memory arrays with async file writes. No mutex or write queue.
**Impact:** Concurrent writes (signups, signouts) can lose data or corrupt the JSON file.
**Remediation:** Add a write lock/queue. Consider SQLite for the auth store.
---
### M11. JSON Parse Errors Silently Swallowed in Body Parser
**File:** `src/servers/_middlewares/body-parser.ts` (lines 2729)
```ts
} catch (ex) {
// throw errors.BAD_REQUEST('Invalid request body');
}
```
**Impact:** Malformed request bodies become `{}`, bypassing input validation in handlers that don't check required fields.
**Remediation:** Uncomment the `throw errors.BAD_REQUEST(...)` line.
---
### M12. Module-Level Token Singleton Race Condition
**File:** `src/workspaces/hooks/src/useClient.ts` (lines 214)
`theToken` is a module-level variable set once per `createClient()` call. Stale after logout.
**Impact:** Authenticated requests may continue after logout in race conditions.
**Remediation:** Read the token fresh from storage in `getHeaders()` rather than caching in a module variable.
---
### M13. No Content Security Policy
**Files:** `src/apps/officer-web/index.html`, `src/servers/hono.ts`
No CSP header or meta tag. Every XSS vulnerability has maximum impact.
**Impact:** No defense-in-depth against script injection.
**Remediation:** Add a CSP header at the server level. At minimum: `default-src 'self'; script-src 'self'; object-src 'none'; base-uri 'self'`.
---
### M14. Gmail Token Cache Is Global, Not Per-User
**File:** `src/servers/queue/handlers/gmail-sync.ts` (lines 4046)
Module-level `cachedAccessToken` shared across all users.
**Impact:** One user's Gmail access token may be used for another user's API calls.
**Remediation:** Scope the cache by user email using a `Map`.
---
## LOW
### L1. `TEST_USERS` Backdoor Mechanism in Signin
**File:** `src/servers/api/auth/signin.ts` (lines 11, 23, 33)
Currently empty, but the code allows bypassing password and passkey verification for listed user IDs.
**Remediation:** Remove the mechanism entirely.
---
### L2. API Keys and OAuth Tokens Stored Unencrypted at Rest
**Files:** `src/servers/api/server-settings/pi-mono.ts` (line 10), `src/databases/officer_db/src/store.ts`
All provider API keys, Google OAuth tokens, and passkey data stored as plaintext JSON.
**Remediation:** Encrypt sensitive values at rest using a key derived from a separate `ENCRYPTION_KEY` env variable.
---
### L3. Sensitive Credentials Returned to Frontend Unnecessarily
**Files:** `src/apps/officer-web/Screens/Dashboard/Settings/IntegrationsSettings/GoogleOAuthConfig.tsx` (line 201), `SMTPSection.tsx`
Google OAuth `clientSecret` and SMTP passwords are fetched to frontend state on every settings page load.
**Remediation:** Return masked values for display. Only require the full value when updating.
---
### L4. Host `$HOME` Path Leaked in API Response
**File:** `src/servers/api/pi/rest.ts` (line 34)
```ts
return ctx.json({ models, providerNames, hostHome: process.env.HOME ?? '' });
```
**Remediation:** Remove `hostHome` from the response or restrict to Super Admin.
---
### L5. Verification Codes Not Cleared from URL
**Files:** `src/apps/officer-web/Screens/Authentication/Verify.tsx` (line 19), `useResetPassword.ts` (line 14)
Codes remain in browser history and `Referer` headers.
**Remediation:** Call `history.replaceState(null, '', window.location.pathname)` after reading the code.
---
### L6. Missing HTTP Security Headers
No `X-Content-Type-Options`, `X-Frame-Options`, `Referrer-Policy`, `Permissions-Policy`, or `Strict-Transport-Security` headers.
**Remediation:** Add a security headers middleware to `hono.ts`.
---
### L7. Package Install Endpoints Lack Role Restriction
**Files:** `src/servers/api/server-settings/claude-code.ts` (line 24), `opencode.ts` (line 47), `pi-mono.ts` (line 275)
Three endpoints run `curl | bash` or `npm install -g` on the host. Even after fixing C3, any authenticated user (not just Super Admin) can trigger installs.
**Remediation:** Restrict to Super Admin. Add integrity checks (SHA256 pinning).
---
### L8. Token Accepted via URL `officerToken` Parameter
**File:** `src/workspaces/hooks/src/useAuth/useAuth.ts` (line 27)
Token in URL is stored permanently in browser history and sent in `Referer` headers. Not cleared from the URL after consumption.
**Remediation:** Use short-lived single-use tokens for embedding. Call `history.replaceState` to remove from URL bar.
---
### L9. Bootstrap Race Condition
**File:** `src/servers/api/auth/auth.ts` (lines 3536)
Two concurrent bootstrap requests could create two Super Admins before `getUserCount()` reflects the first.
**Remediation:** Add a mutex or atomic check-and-insert.
---
## INFO
### I1. No Automated Security Test Coverage
No tests exist for auth flows, authorization, rate limiting, or path traversal guards.
### I2. Stack Traces Logged to Console
**File:** `src/servers/hono.ts` (lines 8990) — full stack traces logged. If logs are user-accessible, this reveals internal paths.
### I3. `passwordChangedAt` Not Checked in WebSocket Upgrade
**File:** `src/server.tsx` (lines 86112) — existing WebSocket connections are not invalidated after password change.
### I4. Docker Container Map File Has No Integrity Protection
**File:** `src/servers/api/terminal/websocket.ts` (line 30) — plain JSON at a known path, tamperable to redirect terminal sessions.
### I5. `preinstall` Script Rejects Non-Node 22 but Project Uses Bun
**File:** `package.json` (line 11) — inconsistent runtime enforcement.
---
## Prioritized Remediation Plan
### Immediate (do first)
1. **Rotate JWT secret** (C1) — generate a 256-bit random value, add startup length check
2. **Fix CORS** (C2) — replace `origin: '*'` with `ALLOWED_ORIGINS` allowlist
3. **Protect server-settings routes** (C3) — move inside `protectedRouter` + Super Admin role check
4. **Protect dev-server proxy** (C4) — require JWT for WebSocket upgrades and HTTP proxy
5. **Remove `rehype-raw` from chat** (C5) — or add `rehype-sanitize` with strict allowlist
### Short-term (next sprint)
6. **Move token to `HttpOnly` cookies** (H1, H2) — eliminates XSS token theft and URL leakage
7. **Add `rehype-sanitize` everywhere** (H3) — all Markdown rendering of user content
8. **Validate URLs for git-clone/yt-dlp/scrape** (H7, M5) — allow only `http://`/`https://`
9. **Enable rate limiting by default** (H9) — disable only in `test` environment
10. **Fix rate limiter key** (H10) — use socket IP, not `X-Forwarded-For`
11. **Sign OAuth state** (H12) — HMAC with `JWT_SECRET`
12. **Reduce `maxRequestBodySize`** (H13) — 100 MB or less
13. **Uncomment body parser error** (M11) — fail loudly on malformed JSON
### Medium-term
14. **Add Content Security Policy** (M13)
15. **Add HTTP security headers** (L6)
16. **Blacklist password reset tokens after use** (M2)
17. **Always validate password complexity** (M3)
18. **Scope Gmail token cache per user** (M14)
19. **Restrict file browser roots** (H8) — remove `~` and `officer.dev` access
20. **Filter AI subprocess environment** (M9) — never pass `JWT_SECRET` or DB credentials
21. **Add schema validation to settings PUT** (M4)
22. **Restrict install endpoints to Super Admin** (L7)
### Longer-term
23. Encrypt sensitive values at rest (L2)
24. Add security test coverage (I1)
25. Implement structured logging with configurable verbosity (I2)
26. Consider SQLite for auth store (M10)
27. Remove `TEST_USERS` mechanism (L1)
-81
View File
@@ -1,81 +0,0 @@
# Security Fixes Log
Tracks remediation progress against findings in `SECURITY_AUDIT.md`.
---
## C1. Weak JWT Secret — FIXED
**Ref:** SECURITY_AUDIT.md > C1
**Changes:**
- `src/servers/jwt.ts` — Added startup guard that throws if `JWT_SECRET` is absent or shorter than 32 characters
- `.env.example` — Updated placeholder to `<generate with: openssl rand -base64 32>`
**Note:** The actual `.env` secret must be rotated manually per deployment.
---
## C2. Global CORS Wildcard on All API Routes — FIXED
**Ref:** SECURITY_AUDIT.md > C2
**Changes:**
- `src/servers/hono.ts` — Replaced `origin: '*'` with dynamic origin validation using the `ALLOWED_ORIGINS` allowlist from `origin-validation.ts`
---
## C3. Server-Settings Routes Entirely Unauthenticated — FIXED
**Ref:** SECURITY_AUDIT.md > C3
**Changes:**
- `src/servers/hono.ts` — Moved `serverSettingsRouter` inside `protectedRouter`
- `src/servers/api/server-settings/server-settings.ts` — Added Super Admin role checks on all write operations
- `src/servers/_middlewares/super-admin-middleware.ts` — New middleware for Super Admin enforcement
- Exempted `GET /api/server-settings/onboarding-complete` as public (mounted before auth)
---
## C4. Unauthenticated Dev-Server WebSocket Proxy — FIXED
**Ref:** SECURITY_AUDIT.md > C4
**Changes:**
- `src/servers/api/dev-server/router.ts`:
- Replaced slug-based proxy URLs with server-generated UUID v4 (`proxyId`). Proxy lookup is now by UUID, eliminating cross-user slug collisions.
- Added `proxyIdIndex` map for O(1) lookup and `pendingStarts` guard against duplicate spawns.
- JWT validation required on HTML requests (`Accept: text/html`) — the iframe entry point. Sub-resources pass on proxyId alone (browsers can't add custom headers to native loads).
- `?token=` query param stripped before forwarding to dev server.
- `proxyOverrideScript` extended to inject `Authorization: Bearer` headers on fetch/XHR and append `?token=` on WebSocket connections, reading fresh tokens from localStorage.
- `src/server.tsx`:
- WebSocket upgrades require `?token=` query param. Token stored in `WSData.wsToken` for deferred async validation in the `open` handler (Bun requires synchronous `server.upgrade()`).
- `devServerWebsocket.open` validates JWT + blacklist check before opening upstream connection. Closes with code `4001` if invalid.
- `src/workspaces/officerdev/src/apps/Preview/PreviewProvider.tsx`:
- `startServer` and status check `useEffect` append `?token=` to iframe URL using `client.token`.
**Security model:** Two-layer capability — proxyId (unguessable UUID) as primary token, JWT as defense-in-depth on entry points (HTML, WebSocket).
---
## C5. AI Chat Messages Rendered with `rehype-raw` — LLM-Driven XSS — FIXED
**Ref:** SECURITY_AUDIT.md > C5
**Changes:**
- Installed `rehype-sanitize` (v6)
- `src/workspaces/officerdev/src/apps/Chat/components/MessageBubble.tsx`:
- Added `rehypeSanitize` with a schema that strips `<script>`, `<iframe>`, `<object>`, `<embed>`, `<form>` tags and all `on*` event handler attributes
- Applied to both `ReactMarkdown` instances (assistant messages + streaming bubble)
- `rehype-raw` retained so legitimate HTML (tables, `<details>`, etc.) still renders — sanitized before hitting the DOM
---
## Remaining
See `SECURITY_AUDIT.md` for the full list. Next priorities:
- **H3** — `rehype-sanitize` for the other 6 Markdown rendering locations
- **H1/H2** — Move token to `HttpOnly` cookies
- **H7/M5** — URL validation for git-clone/yt-dlp/scrape
- **H9/H10** — Rate limiting fixes
-286
View File
@@ -1,286 +0,0 @@
# Officer Setup Guide
## Overview
Officer is a self-hosted AI intranet for teams. This guide covers fresh installation on Ubuntu/Debian servers.
## Architecture
Officer uses **system-wide Node.js v22** to ensure all users have consistent access:
```
System Node.js v22 (/usr/bin/node)
All users automatically get:
├── node, npm, bun
├── pi, claude (coding agents)
├── go, rustc (compilers)
└── All tools and services
Systemd Services (use system Node)
├── officer-pty-sidecar
├── officer (main server)
└── (other services)
```
**Benefits:**
- One Node version for everyone (no conflicts)
- New users automatically get same setup
- Services use consistent environment
- Simple to maintain and troubleshoot
- Automatic installation (setup.sh handles it)
---
## Installation Steps
### Quick Start (One Command)
```bash
cd /path/to/officer/monorepo
bash scripts/setup.sh
```
That's it! The script will:
- Install Node.js 22 automatically (if not found)
- Install all dependencies
- Setup all services
- Verify everything works
### What Happens Automatically
The setup script automatically:
1. Checks if Node 22 is installed
2. If not, sets up NodeSource repository and installs it
3. Installs all system packages, tools, and npm globals
4. Creates systemd services
5. Verifies everything works
**No manual Node installation needed!**
### Coming from nvm?
```bash
# If you previously installed via nvm, you can remove it
# (optional, won't interfere)
rm -rf ~/.nvm
# Or keep it for local dev, but system Node will take precedence for services
```
The system Node (installed by setup.sh) will be used by:
- All users
- All systemd services
- All npm global packages
- Officer server itself
This will:
- Install all system dependencies (git, build tools, ffmpeg, etc.)
- Install Bun, Go, Rust
- Install npm global packages (pi, claude-code, etc.)
- Setup PulseAudio for audio
- Setup remote desktop (XFCE + VNC)
- Configure PTY sidecar systemd service
- Verify everything works
**Note:** Setup script uses `sudo` for system-level installations. You'll be prompted for your password.
### Verify Installation
After setup.sh completes, verify as any user:
```bash
node --version # v22.x.x
which pi # /usr/bin/pi
which claude # /usr/bin/claude
which bun # /usr/local/bin/bun
# Test Pi works
pi --list-models
# Test Officer can start
bun dev
# In another terminal, test API
curl http://localhost:5000/api/pi/models
```
### Start Officer
**Development:**
```bash
bun dev
```
**Production (with PM2):**
```bash
pm2 start ecosystem.config.cjs
pm2 logs officer
```
**Production (with systemd):**
```bash
sudo systemctl start officer
sudo systemctl status officer
```
---
## Multi-User Scenarios
### Scenario A: Server with Multiple Users
All users automatically get access to:
- `node` command (system-wide)
- `pi`, `claude` (system-wide npm packages)
- Officer web UI (via port/proxy)
**No setup needed per user.** Just install globally once as shown above.
```bash
# User 1
$ which node
/usr/bin/node
# User 2 (different terminal/server)
$ which node
/usr/bin/node
# Both work!
```
### Scenario B: Development User Wants nvm
**Important:** For production, DON'T do this. But for dev, you can:
```bash
# As a user (not system-wide)
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash
nvm install 22
nvm use 22
# Now you can use nvm locally, but make sure system Node is also installed for services
which node # /home/user/.nvm/versions/node/v22.x.x/bin/node (your local override)
```
The `officer-pty-sidecar` systemd service will still use `/usr/bin/node` (system-wide).
---
## Troubleshooting
### "node: command not found"
This shouldn't happen if setup.sh ran successfully. But if it does:
```bash
# Run setup script again — it will install Node if missing
bash scripts/setup.sh
# Or manually install:
curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash -
sudo apt-get install -y nodejs
# Verify
node --version # Should be v22.x.x
```
### "pi: command not found"
```bash
# Pi not installed
sudo npm install -g @mariozechner/pi-coding-agent
# Or use setup script
bash scripts/setup.sh
```
### "Pi process exited with code 1" in chat
This usually means **snap node was used**. Don't use snap node for Officer.
```bash
# Check if snap node is installed
which node
# If output contains "/snap/bin/node", remove it:
sudo snap remove node
# Then install system Node
curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash -
sudo apt-get install -y nodejs
# Verify
which node # /usr/bin/node (NOT /snap/bin/node)
```
### PTY Sidecar service won't start
```bash
# Check status
sudo systemctl status officer-pty-sidecar
# Check logs
journalctl -u officer-pty-sidecar -n 20
# Verify Node is installed
/usr/bin/node --version
# Restart
sudo systemctl restart officer-pty-sidecar
```
---
## Files Changed/Created
- `scripts/setup.sh` — Main installation script (updated to use system Node)
- `scripts/setup-pty-sidecar.sh` — PTY sidecar systemd service setup (updated)
- `ecosystem.config.cjs` — PM2 config (no changes, using systemd instead)
- `SETUP_GUIDE.md` — This file
---
## What NOT To Do
**Don't use snap node**
- It has file descriptor issues with piped processes
- Use system Node via NodeSource instead
**Don't mix Node versions per user**
- For production: one Node version for everyone (system-wide)
- For dev: sure, use nvm, but keep system Node installed too
**Don't install npm packages locally for system services**
- Services need system-wide packages (`sudo npm install -g`)
- Or use absolute paths in service files
**Do:**
- Use system Node 22 via NodeSource
- Install global packages once with `sudo npm install -g`
- Use systemd services for daemons
- Let all users share the same tools
---
## Quick Start
```bash
cd /path/to/officer/monorepo
bash scripts/setup.sh
bun dev
```
Open browser: `http://localhost:5000` (or your configured port)
That's it! Everything is installed automatically.
---
## Support
If you hit issues:
1. Check troubleshooting section above
2. Verify system Node is installed: `node --version`
3. Check service logs: `journalctl -u officer-pty-sidecar`
4. Re-run setup script: `bash scripts/setup.sh`
+7 -10
View File
@@ -5,14 +5,11 @@ Deferred work. Context: Officer is collapsing from multi-tenant / open-source-re
## Single-user cleanup ## Single-user cleanup
- [ ] **Remove dead `username` plumbing.** `pi-bridge.spawnPi` no longer uses `username` — it always - [ ] **Remove dead `username` plumbing.** Mostly resolved by deleting the chat channels — the
runs as the service user. But the handlers still compute `toShellUsername(...)` and thread it handlers and `send-and-await.ts` that threaded `toShellUsername(...)` through to nothing are gone.
through `sendAndAwait``spawnOptions`, where nothing reads it. `send-claude-code` declares What remains: `send-claude-code.ts` still declares `username` without using it, and
`username` (lines 8, 36) without using it. Typechecks clean today (excess-property checks don't `toShellUsername` has one real caller left (`provision.ts``generateClaudeSettings`), so it may
fire on variables), so it silently *looks* load-bearing. be inlinable.
Touches: `channels/{whatsapp,telegram,discord}/handler.ts`, `channels/send-and-await.ts`,
`channels/send-claude-code.ts`. After this, `toShellUsername` has one caller left
(`provision.ts``generateClaudeSettings`) and may be inlinable.
- [x] **Delete or gut `scripts/provision-existing-users.sh`.** Done — deleted the script (it ran - [x] **Delete or gut `scripts/provision-existing-users.sh`.** Done — deleted the script (it ran
`sudo useradd …`, the source of the vestigial `andrepadez`/`john-wick`/`fedra`/`miguelbenoliel` `sudo useradd …`, the source of the vestigial `andrepadez`/`john-wick`/`fedra`/`miguelbenoliel`
@@ -28,7 +25,7 @@ Deferred work. Context: Officer is collapsing from multi-tenant / open-source-re
## Email ## Email
- [x] **Gmail-style search operators** (done 2026-07-24, `0e5b91e`). `parseEmailQuery` + - [x] **Gmail-style search operators** (done 2026-07-24, `0e5b91e`). `parseEmailQuery` +
`searchEmails` in `api/email/email-db.ts` parse: `from:`/`to:`/`subject:`/`body:` → FTS5 column `searchEmails` in `sidecar/email/store.ts` parse: `from:`/`to:`/`subject:`/`body:` → FTS5 column
filters; `has:attachment`, `is:unread`/`is:read`, `label:X`, `before:`/`after:YYYY-MM-DD` → SQL filters; `has:attachment`, `is:unread`/`is:read`, `label:X`, `before:`/`after:YYYY-MM-DD` → SQL
`WHERE` on `emails`; quoted values → exact phrases; free text → prefix-AND full-text; unknown `WHERE` on `emails`; quoted values → exact phrases; free text → prefix-AND full-text; unknown
`op:val` falls back to free text. Structured-filters-only queries skip the FTS join. Validated `op:val` falls back to free text. Structured-filters-only queries skip the FTS join. Validated
@@ -45,7 +42,7 @@ Deferred work. Context: Officer is collapsing from multi-tenant / open-source-re
- [x] **Conversation threading** (done 2026-07-24, `ee11c94`). `thread_id` column on `emails`: - [x] **Conversation threading** (done 2026-07-24, `ee11c94`). `thread_id` column on `emails`:
header-based for new mail (`id` = `sha1(Message-Id)`, so `References[0]` hashes to the root's header-based for new mail (`id` = `sha1(Message-Id)`, so `References[0]` hashes to the root's
id — `computeThreadId` in `email-db.ts`), subject+counterpart backfill for already-synced mail. id — `computeThreadId` in `sidecar/email/store.ts`), subject+counterpart backfill for already-synced mail.
`/messages` collapses to one row per thread (window fn) with count/unread; `/thread/:id` + `/messages` collapses to one row per thread (window fn) with count/unread; `/thread/:id` +
`/thread/:id/read`; reader renders a collapsible stack. Verified on synthetic + 18k real DB. `/thread/:id/read`; reader renders a collapsible stack. Verified on synthetic + 18k real DB.
- [ ] **Upgrade old mail to exact threading.** One-time full re-fetch to capture `References` for - [ ] **Upgrade old mail to exact threading.** One-time full re-fetch to capture `References` for
-404
View File
@@ -1,404 +0,0 @@
# Officer Platform — Full Dockerization Plan
## Current State
The platform runs directly on an Ubuntu host with the following components:
### Processes (managed by PM2)
| Process | Runtime | Entry Point |
|---------|---------|-------------|
| officer (main API) | bun | `src/server.tsx` |
| officer-claude | bun | `src/servers/sidecar/claude/index.ts` |
| officer-pi | bun | `src/servers/sidecar/pi/index.ts` |
| officer-email | bun | `src/servers/sidecar/email/index.ts` |
| officer-pty | node | `src/servers/api/terminal/pty-sidecar.mjs` |
| officer-vnc | bun | `src/servers/sidecar/vnc/index.ts` |
### Already Dockerized (docker-compose.yaml)
- Nginx Proxy Manager
- PostgreSQL 18
- Mailhog
- Redis
- SearXNG
- static-files (nginx)
- landing-page (nginx)
### System Dependencies
- **Runtimes:** Bun, Node.js 22, Go, Rust
- **CLI tools:** pi, claude, ffmpeg, yt-dlp, git, mbsync, cliamp
- **Audio:** PulseAudio, pactl, libasound2-dev, libvorbis-dev, libogg-dev, libflac-dev
- **Desktop:** XFCE4, TigerVNC, Brave browser, dbus-x11
- **Shell:** zsh, oh-my-zsh, starship, neovim, ripgrep, fd, tmux, htop, btop
- **Build:** build-essential, pkg-config, python3
- **Browser:** Chrome (Puppeteer, for WhatsApp bot)
---
## Target State
Everything runs via `docker compose up`. Migration = copy volumes + `docker compose up` on new host.
---
## Proposed Container Architecture
### Container 1: `officer-core`
**What:** Main API server + all sidecars (claude, pi, email)
**Why combined:** Sidecars communicate with the main server over localhost WebSocket. Splitting them into separate containers adds networking complexity with no real benefit — they share the same codebase and dependencies.
**Base image:** `ubuntu:24.04` (not Alpine — too many native deps)
**Includes:**
- Bun runtime
- Node.js 22 (for PTY sidecar, Pi agent)
- Pi coding agent (`@mariozechner/pi-coding-agent`)
- Claude Code (`@anthropic-ai/claude-code`)
- ffmpeg, git, zsh, ripgrep, fd, jq
- Chrome for Testing (Puppeteer — WhatsApp bot)
- mbsync (Gmail IMAP sync)
- yt-dlp
- build-essential, pkg-config, python3 (for native modules like node-pty, argon2)
- PM2 (process manager for main + sidecars)
**Entry point:** `pm2-runtime ecosystem.config.cjs`
**Ports:**
- `9010` — Main API + WebSocket
- `18792` — Browser relay
**Volumes:**
- `./data:/app/data` — DATA_PATH (all user data, sessions, attachments)
- `./pi-config:/root/.pi/agent` — Pi agent auth + models config
**Environment:**
- `PORT`, `JWT_SECRET`, `POSTGRES_URL`, `MAIL_TRANSPORT`, `PUBLIC_URL`, `DATA_PATH=/app/data`, `HOME_DIR=/app/data/superadmin/home`
### Container 2: `officer-pty`
**What:** Terminal/PTY sidecar
**Why separate:** This is the only component that needs `sudo` and Linux user isolation. It creates real Linux users and spawns shells as those users. Running this inside the core container would require the core container to run as root with full user management capabilities.
**Base image:** `ubuntu:24.04`
**Includes:**
- Node.js 22 (node-pty requires Node, not Bun)
- zsh, oh-my-zsh, starship, neovim
- Shell utilities (ripgrep, fd, tmux, htop, btop, tree, git)
- Go, Rust (available in user shells)
- sudo, useradd (user provisioning)
**Entry point:** `node src/servers/api/terminal/pty-sidecar.mjs`
**Privileged:** Yes — needs to create users, spawn shells as different users
**Volumes:**
- `./data:/app/data` — Shared DATA_PATH (user home dirs live here)
**Network:** Connects to officer-core via WebSocket (`ws://officer-core:9010/api/sidecar/register`)
### Container 3: `officer-vnc`
**What:** VNC/remote desktop sidecar
**Why separate:** Requires a full X11 stack (XFCE4, TigerVNC, Brave, dbus). This is ~2GB+ of packages. Keeping it separate means the core image stays lean, and you can skip this container entirely if desktop isn't needed.
**Base image:** `ubuntu:24.04`
**Includes:**
- XFCE4, xfce4-goodies
- TigerVNC server
- Brave browser
- dbus-x11
- Bun (to run the sidecar code)
- sudo, useradd (VNC sessions run as individual users)
**Entry point:** `bun run src/servers/sidecar/vnc/index.ts`
**Privileged:** Yes — spawns VNC servers as different users
**Ports:**
- `5901-5999` — VNC display ports (dynamic)
**Volumes:**
- `./data:/app/data` — User home dirs (VNC configs stored per-user)
**Network:** Connects to officer-core via WebSocket
### Container 4: `officer-audio`
**What:** PulseAudio + cliamp audio sidecar
**Why separate:** PulseAudio daemon needs to run persistently. Audio dependencies (libvorbis, libogg, libflac, libasound) are specific to this feature.
**Base image:** `ubuntu:24.04`
**Includes:**
- PulseAudio (headless, null sink)
- cliamp binary (pre-built or built from source with Go)
- libasound2-dev, libvorbis-dev, libogg-dev, libflac-dev
- `script` command (bsdutils — for PTY wrapping)
**Entry point:** Start PulseAudio daemon, then expose audio endpoint
**Note:** This is the trickiest container. See pain points below.
---
## Pain Points & Challenges
### 1. User Isolation (PTY + VNC)
**Problem:** The platform creates real Linux users (`useradd`) and spawns shells/VNC sessions as those users via `sudo -u`. Inside Docker, this requires:
- Running as root
- The `--privileged` flag or specific capabilities
- Shared `/etc/passwd`, `/etc/shadow` between PTY and VNC containers (both need to know about the same users)
**Options:**
- **A) Shared user database:** Mount a shared `/etc/passwd` and `/etc/shadow` between PTY and VNC containers. Both can create users there. Risk: concurrent writes.
- **B) User provisioning service:** A small API that manages Linux users, called by both PTY and VNC sidecars.
- **C) Pre-provision users:** At startup, scan DATA_PATH for existing user directories and create Linux users for each. New users get created on-demand.
- **D) Drop real user isolation:** Map all users to a single Linux user, rely on application-level sandboxing only. Simpler but less secure.
**Recommendation:** Option C with a shared init script. Both containers run the same user-provisioning logic at startup.
### 2. PulseAudio in Docker
**Problem:** PulseAudio expects to manage audio hardware. In a headless Docker container, there's no hardware.
**Options:**
- **A) Null sink approach (current):** Already works — `pactl load-module module-null-sink`. The container just needs PulseAudio running with a null sink. cliamp writes to it, and the audio is captured and streamed over WebSocket.
- **B) PulseAudio socket sharing:** Run PulseAudio on the host, mount the socket into the container.
**Recommendation:** Option A. It already works headless. Just ensure the Dockerfile starts PulseAudio before cliamp.
### 3. Audio Integration with Core
**Problem:** Currently, the `/api/cliamp/audio/ws` endpoint is part of the main server (`src/server.tsx`). It spawns `cliamp` as a child process. If audio moves to a separate container, the WebSocket endpoint and the cliamp process are in different containers.
**Options:**
- **A) Keep audio in core container:** Don't split it out. Add PulseAudio + cliamp deps to the core image. Simplest, but adds ~200MB to the core image.
- **B) Audio sidecar with WebSocket proxy:** The audio container runs its own WebSocket server. The core container proxies `/api/cliamp/*` to it. More complex.
- **C) Audio as a registered sidecar:** Like the other sidecars, audio registers over WebSocket and handles commands. The cleanest architecture but requires refactoring the cliamp code.
**Recommendation:** Option A for now (keep in core). Refactor to Option C later if the image size becomes a problem.
### 4. Chrome/Puppeteer (WhatsApp Bot)
**Problem:** Chrome needs specific flags (`--no-sandbox`, `--disable-dev-shm-usage`) and a larger `/dev/shm` in Docker.
**Solution:** Well-understood. Add to docker-compose:
```yaml
shm_size: '2gb'
```
And install Chrome for Testing via Puppeteer's CLI in the Dockerfile.
### 5. VNC Port Range
**Problem:** VNC dynamically allocates ports 5901-5999 based on available displays. Docker needs these ports mapped.
**Options:**
- **A) Fixed port range:** Map `5901-5910` (10 concurrent sessions max). Simple.
- **B) Host networking:** Use `network_mode: host` for the VNC container. No port mapping needed but less isolation.
- **C) noVNC proxy:** Add a noVNC WebSocket proxy. Clients connect via a single HTTP port, no VNC port range needed. Better for firewalls/reverse proxies.
**Recommendation:** Option C (noVNC) is the best long-term solution. Option A works for now.
### 6. Shared Source Code
**Problem:** All containers need the same source code (sidecars import from the same codebase). Building separate images from the same repo means either:
- Copying the full source into each image (duplicated, large)
- Using a multi-stage build with a shared base
- Mounting the source as a volume (dev only, not portable)
**Recommendation:** Multi-stage Dockerfile. One `base` stage installs OS deps + runtimes + `bun install`. Subsequent stages extend it with container-specific deps (XFCE for VNC, PulseAudio for audio, etc.).
### 7. DATA_PATH Permissions
**Problem:** Multiple containers write to the same DATA_PATH volume. If containers run as different UIDs, file permissions conflict.
**Solution:** All containers should run processes as the same UID (e.g., 1000). The PTY and VNC containers additionally need root to manage sub-users, but the data files should be owned by a consistent UID.
### 8. Pi and Claude Config Directories
**Problem:** Pi reads `~/.pi/agent/auth.json` and `~/.pi/agent/models.json`. Claude reads `~/.claude/`. These are in the container's home directory, which is ephemeral.
**Solution:** Mount these as volumes:
```yaml
volumes:
- ./config/pi:/home/officer/.pi/agent
- ./config/claude:/home/officer/.claude
```
### 9. Native Modules (node-pty, argon2)
**Problem:** `node-pty` and `argon2` have native C/C++ bindings. They need to be compiled for the container's architecture, not the host's.
**Solution:** Run `bun install` inside the Dockerfile (not on the host). The Dockerfile must include `build-essential`, `python3`, and `pkg-config`.
### 10. Image Size
**Concern:** The core image will be large due to Bun + Node.js + Chrome + native deps. The VNC image will be even larger (XFCE alone is ~1.5GB).
**Estimated sizes:**
- `officer-core`: ~2-3 GB (Bun, Node, Chrome, ffmpeg, native deps)
- `officer-pty`: ~1-1.5 GB (Node, shell tools, Go, Rust)
- `officer-vnc`: ~2.5-3 GB (XFCE, TigerVNC, Brave, Bun)
- `officer-audio`: ~500 MB (PulseAudio, cliamp, Go)
**Mitigation:** Multi-stage builds, `.dockerignore`, shared base layers.
---
## Proposed docker-compose.yaml Structure
```yaml
services:
# --- Infrastructure (already dockerized) ---
nginx-proxy-manager:
image: jc21/nginx-proxy-manager:latest
# ... (unchanged)
postgres:
image: postgres:18-alpine
# ... (unchanged)
redis:
image: redis:alpine
# ... (unchanged)
mailhog:
image: mailhog/mailhog:latest
# ... (unchanged)
searxng:
image: searxng/searxng:latest
# ... (unchanged)
static-files:
image: nginx:alpine
# ... (unchanged)
landing-page:
image: nginx:alpine
# ... (unchanged)
# --- Application ---
officer-core:
build:
context: .
dockerfile: docker/Dockerfile.core
container_name: officer-core
restart: unless-stopped
ports:
- "9010:9010"
- "18792:18792"
environment:
- PORT=9010
- NODE_ENV=production
- DATA_PATH=/app/data
- HOME_DIR=/app/data/superadmin/home
- POSTGRES_URL=postgresql://postgres:${PG_PASSWORD}@postgres:5432/officer_dev
- MAIL_TRANSPORT=smtp://mailhog:1025
- PUBLIC_URL=${PUBLIC_URL}
- JWT_SECRET=${JWT_SECRET}
- BROWSER_RELAY_PORT=18792
- DISCORD_BUG_REPORT_WEBHOOK=${DISCORD_BUG_REPORT_WEBHOOK}
volumes:
- officer-data:/app/data
- pi-config:/home/officer/.pi/agent
shm_size: '2gb'
networks:
- services
depends_on:
- postgres
- redis
officer-pty:
build:
context: .
dockerfile: docker/Dockerfile.pty
container_name: officer-pty
restart: unless-stopped
privileged: true
environment:
- API_URL=ws://officer-core:9010/api/sidecar/register
- DATA_PATH=/app/data
volumes:
- officer-data:/app/data
networks:
- services
depends_on:
- officer-core
officer-vnc:
build:
context: .
dockerfile: docker/Dockerfile.vnc
container_name: officer-vnc
restart: unless-stopped
privileged: true
ports:
- "5901-5910:5901-5910"
environment:
- API_URL=ws://officer-core:9010/api/sidecar/register
- DATA_PATH=/app/data
volumes:
- officer-data:/app/data
networks:
- services
depends_on:
- officer-core
volumes:
officer-data:
pi-config:
networks:
services:
external: true
```
---
## Migration Path
### Phase 1: Core container
1. Write `Dockerfile.core` — Bun, Node, Pi, Claude, Chrome, ffmpeg
2. Move the `.env` config to docker-compose environment variables
3. Update `POSTGRES_URL` to use Docker service name (`postgres` not `localhost`)
4. Update `MAIL_TRANSPORT` to use Docker service name (`mailhog`)
5. Test: main server + claude/pi/email sidecars work in container
6. Test: WhatsApp bot (Puppeteer/Chrome) works
### Phase 2: PTY container
1. Write `Dockerfile.pty` — Node, shell tools, user management
2. Refactor PTY sidecar to connect to remote API_URL (currently assumes localhost)
3. Implement user provisioning in container startup
4. Test: terminal sessions work from the web UI
### Phase 3: VNC container
1. Write `Dockerfile.vnc` — XFCE, TigerVNC, Brave
2. Refactor VNC sidecar for remote API_URL
3. Shared user provisioning with PTY container
4. Test: remote desktop sessions work
### Phase 4: Audio (optional)
1. If kept in core: add PulseAudio + cliamp deps to `Dockerfile.core`
2. If separate: write `Dockerfile.audio` and refactor to sidecar protocol
3. Test: music playback works
### Phase 5: Cleanup
1. Remove `scripts/setup.sh` system-level deps (no longer needed)
2. Write backup/restore scripts for Docker volumes
3. Document the new deployment process
4. CI/CD: build images and push to registry
---
## What Stays Outside Docker
- **Nginx Proxy Manager** — already dockerized, unchanged
- **DNS / Tailscale** — host-level networking
- **UFW** — host firewall
- **Hetzner backups** — host-level
- **Cron jobs (pg_dump)** — can move into a container or stay on host
- **SSL certificates** — managed by NPM
---
## Open Questions
1. **Should Go and Rust be in the PTY container?** Users may want to compile Go/Rust projects from the terminal. But it adds significant image size. Could be made optional via a build arg.
2. **Should cliamp be pre-built or built from source?** Building from source requires Go + audio dev headers in the image. A pre-built binary would be smaller but architecture-specific.
3. **How to handle Pi/Claude version updates?** Currently `npm install -g` on the host. In Docker, this means rebuilding the image. Could mount a volume for global npm packages, but that adds complexity.
4. **Do we need a container registry?** For single-server deployments, `docker compose build` is fine. For multi-server, you'd want to push images to a registry (GitHub Container Registry, Docker Hub, self-hosted).
5. **Dev workflow:** How to develop locally? Mount source code as volume + hot reload? Or just develop on host and only use Docker for deployment?
+5
View File
@@ -1,5 +1,10 @@
# Jobs Unification — making every task a background job # Jobs Unification — making every task a background job
**Status: shipped, except push notifications.** Phases 13 (data model, executor, queue, REST API and
the master-detail `/jobs` screen) are done and live. Phase 4 is the only open item, and is the reason
this file still exists. Email sync is deliberately NOT part of this any more — it moved into the email
sidecar with its own scheduling, so it does not appear in the Jobs list.
**Goal:** every task run (script, pipeline, later agentic) becomes a persisted, background **job** **Goal:** every task run (script, pipeline, later agentic) becomes a persisted, background **job**
created over REST, streamed live over WebSocket, resumable/attachable, visible on desktop *and* phone, created over REST, streamed live over WebSocket, resumable/attachable, visible on desktop *and* phone,
and ending in a push notification. Replaces today's ephemeral script-task WebSocket path. and ending in a push notification. Replaces today's ephemeral script-task WebSocket path.
+9 -4
View File
@@ -14,7 +14,7 @@ officer/
``` ```
Officer is a self-hosted personal platform: an AI agent, a terminal, a file browser, a code editor, Officer is a self-hosted personal platform: an AI agent, a terminal, a file browser, a code editor,
email, chat channels, a remote desktop and dashboards, behind one web app. **It serves exactly one email, a bitcoin wallet, a remote desktop and dashboards, behind one web app. **It serves exactly one
person — the owner of this server.** There is no tenancy, no roles, no other users. If a question person — the owner of this server.** There is no tenancy, no roles, no other users. If a question
turns on "which user", the answer is the owner. turns on "which user", the answer is the owner.
@@ -59,11 +59,16 @@ Commit messages: simple lowercase, no prefixes, explaining *why*.
The server runs under pm2 as `officer`, plus sidecars (`officer-anthropic-proxy`, `officer-agent`, The server runs under pm2 as `officer`, plus sidecars (`officer-anthropic-proxy`, `officer-agent`,
`officer-opencode`, `officer-email`, `officer-pty`, `officer-vnc`, `officer-music`, `officer-vault`, `officer-opencode`, `officer-email`, `officer-pty`, `officer-vnc`, `officer-music`, `officer-vault`,
`officer-slskd`). `pm2 list` shows them; `pm2 logs officer` follows. `officer-slskd`, `officer-headscale`, `officer-transmission`, `officer-invoiceshelf`, `officer-wallet`).
`pm2 list` shows them; `pm2 logs officer` follows.
Two of those names are worth knowing apart: **`officer-anthropic-proxy` holds the Anthropic credential Two of those names are worth knowing apart: **`officer-anthropic-proxy` holds the Anthropic credential
and proxies API traffic; `officer-agent` is the process that actually runs `claude`.** They used to be and proxies API traffic; `officer-agent` is the process that actually runs `claude`.**
one confusingly-named entry (`officer-claude`) that was only the proxy.
**Which process to restart.** A change under `src/servers/sidecar/<name>/` needs that sidecar restarted;
a change anywhere else needs `officer`. Both, if you changed the wire between them. Restarting `officer`
no longer costs you a running agent session or a terminal — every sidecar is a PM2 peer, and the terminal
and email sidecars serve their own listeners, so `officer` is not in their data path at all.
**Don't restart the owner's server to test.** Boot your own on a spare port instead — the running **Don't restart the owner's server to test.** Boot your own on a spare port instead — the running
instance holds `PORT` from `.env` (9010): instance holds `PORT` from `.env` (9010):