From 8b2ff1c05ce427903ad19a28c7f37836bef1a22a Mon Sep 17 00:00:00 2001 From: Muhammad Ahmed Cheema Date: Wed, 15 Apr 2026 20:51:21 -0700 Subject: [PATCH 1/3] chore: v0.19.0 release - chat channel, docs, Docker infrastructure Version bump to 0.19.0 for the Project 4 chat channel release. Docker infrastructure: - Dockerfile: public-defaults backup for entrypoint seeding - Entrypoint: sync chat-ui SPA and base templates into phantom_public volume on every start, chown to uid 999 - .dockerignore: exclude chat-ui/node_modules, dist, .vite - docker-compose.user.yaml: add oom_score_adj, cpu_shares, pids hardening - .env.example: add OWNER_EMAIL for Slack-less web chat login Documentation: - README: chat channel features, updated test/version badges - CLAUDE.md: tech stack, project structure, build commands, architecture - channels.md: full Web Chat section - getting-started.md: OWNER_EMAIL for no-Slack setup - security.md: chat auth, VAPID, attachment security - architecture.md: chat data flow, two-transcript invariant - deploy-checklist.md: chat-ui bare metal steps - docker-deploy.md: entrypoint seeding explained - CONTRIBUTING.md: test count, chat-ui build commands - memory.md: shared memory across channels Version references updated in: package.json, server.ts, mcp/server.ts, cli/index.ts, CLAUDE.md, README.md --- .dockerignore | 3 +++ .env.example | 10 ++++++++ CLAUDE.md | 33 ++++++++++++++++++++++---- CONTRIBUTING.md | 7 +++++- Dockerfile | 3 +++ README.md | 17 ++++++------- docker-compose.user.yaml | 3 +++ docs/architecture.md | 22 +++++++++++++---- docs/channels.md | 46 +++++++++++++++++++++++++++++++++++- docs/deploy-checklist.md | 14 +++++++++++ docs/docker-deploy.md | 4 +++- docs/getting-started.md | 27 +++++++++++++++++++-- docs/memory.md | 2 ++ docs/security.md | 27 +++++++++++++++++++++ package.json | 2 +- scripts/docker-entrypoint.sh | 14 +++++++++++ src/chat/types.ts | 2 +- src/cli/index.ts | 2 +- src/core/server.ts | 2 +- src/mcp/server.ts | 2 +- 20 files changed, 216 insertions(+), 26 deletions(-) diff --git a/.dockerignore b/.dockerignore index 6a46ccb1..7e64fcf4 100644 --- a/.dockerignore +++ b/.dockerignore @@ -12,3 +12,6 @@ local/ *.db-shm .DS_Store bun.lockb +chat-ui/node_modules +chat-ui/dist +chat-ui/.vite diff --git a/.env.example b/.env.example index e74e8649..55311aef 100644 --- a/.env.example +++ b/.env.example @@ -34,6 +34,16 @@ ANTHROPIC_API_KEY= # Owner's Slack user ID (starts with U) - only this user can talk to Phantom # OWNER_SLACK_USER_ID= +# ======================== +# OPTIONAL: Web Chat Login +# ======================== +# If Slack is not configured, Phantom can send you a magic link email +# to log into the web chat at /chat. Set your email and a Resend API key. +# Without Resend, a bootstrap token is printed to container logs instead. + +# OWNER_EMAIL= +# RESEND_API_KEY is also used for the phantom_email tool (see below). + # ======================== # OPTIONAL: Identity # ======================== diff --git a/CLAUDE.md b/CLAUDE.md index 4f482f5f..d250b3c4 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,6 +1,6 @@ # Phantom -Phantom is an autonomous AI co-worker that runs as a persistent Bun process on a VM. It wraps the Claude Agent SDK as a subprocess (Anthropic by default, swappable via a `provider:` config block to Z.AI/GLM-5.1, OpenRouter, Ollama, vLLM, LiteLLM, or any Anthropic Messages API compatible endpoint). It maintains vector-backed memory across sessions, rewrites its own configuration through a validated self-evolution engine, communicates via Slack/Telegram/Email/Webhook, and exposes all capabilities as an MCP server. 27,000+ lines of TypeScript, 875 tests, v0.18.2. Apache 2.0, repo at ghostwright/phantom. +Phantom is an autonomous AI co-worker that runs as a persistent Bun process on a VM. It wraps the Claude Agent SDK as a subprocess (Anthropic by default, swappable via a `provider:` config block to Z.AI/GLM-5.1, OpenRouter, Ollama, vLLM, LiteLLM, or any Anthropic Messages API compatible endpoint). It maintains vector-backed memory across sessions, rewrites its own configuration through a validated self-evolution engine, communicates via Slack/Web Chat/Telegram/Email/Webhook, and exposes all capabilities as an MCP server. 30,000+ lines of TypeScript, 1,584 tests, v0.19.0. Apache 2.0, repo at ghostwright/phantom. ## Tech Stack @@ -10,7 +10,8 @@ Phantom is an autonomous AI co-worker that runs as a persistent Bun process on a | Agent | Claude Agent SDK (`@anthropic-ai/claude-agent-sdk`) subprocess. Provider is configurable via `src/config/providers.ts`: Anthropic (default), Z.AI, OpenRouter, Ollama, vLLM, LiteLLM, custom. | | Memory | Qdrant (vector DB, Docker) + Ollama (nomic-embed-text, local embeddings) | | State | SQLite via Bun (sessions, tasks, metrics, evolution versions, scheduled jobs) | -| Channels | Slack (Socket Mode, primary), Telegram (long polling), Email (IMAP/SMTP), Webhook (HMAC-SHA256), CLI | +| Channels | Slack (Socket Mode), Web Chat (SSE streaming), Telegram (long polling), Email (IMAP/SMTP), Webhook (HMAC-SHA256), CLI | +| Chat Client | React 19 + Vite + shadcn/ui + Tailwind v4 SPA at `/chat` | | Web UI | Tailwind v4 Browser CDN + DaisyUI v5, static files from public/ | | MCP | Streamable HTTP on /mcp, bearer token auth, 17+ tools | | Infrastructure | Docker (compose), Specter VMs (Hetzner), systemd (bare metal) | @@ -41,13 +42,22 @@ If you find yourself writing a function that does something the agent can do bet ```bash bun install # Install dependencies -bun test # Run 875 tests +bun test # Run 1,584 tests bun run src/index.ts # Start the server bun run src/cli/main.ts init --yes # Initialize config (reads env vars) bun run src/cli/main.ts doctor # Check all subsystems bun run src/cli/main.ts status # Quick one-liner status bun run lint # Biome check bun run typecheck # tsc --noEmit + +# Chat UI (separate build) +cd chat-ui && bun install # Install chat-ui dependencies +cd chat-ui && bun run build # Build production SPA to chat-ui/dist/ +cd chat-ui && bun run typecheck # Type-check the chat client +cd chat-ui && bun run dev # Dev server on :5173 (proxy to :3100) + +# Chat-UI dev loop: run Phantom on :3100 in one terminal, Vite on :5173 in another. +# Vite proxies /chat/* API calls to :3100. Open http://localhost:5173/chat. ``` ## Project Structure @@ -60,6 +70,19 @@ src/ prompt-assembler.ts # System prompt: base + role + evolved + memory context hooks.ts # Safety hooks (dangerous command blocker, file tracker) in-process-tools.ts # In-process MCP tool servers (dynamic, scheduler, web UI) + chat/ + http.ts # SSE endpoint, session management, message routing + http-handlers.ts # Request handlers for chat API routes + types.ts # 32-event SSE wire format (discriminated union) + sdk-to-wire.ts # Translates Agent SDK events to chat wire frames + session-store.ts # In-memory chat session state + message-store.ts # Persistent message history (SQLite) + stream-bus.ts # Fan-out SSE event bus (multi-tab support) + upload.ts # File attachment upload handler + validators.ts # Type allowlist and size limits for attachments + first-run.ts # Email-based first login (Resend) + email-login.ts # Magic link email delivery + notifications/ # Web Push (VAPID keys, subscriptions, triggers) channels/ slack.ts # Slack Socket Mode (primary channel, owner access control) telegram.ts # Telegram via Telegraf @@ -116,6 +139,7 @@ src/ config/ # YAML configs (phantom.yaml, channels.yaml, mcp.yaml, roles/) phantom-config/ # Evolved agent config (constitution, persona, domain knowledge) public/ # Web UI files (_base.html template, index.html) +chat-ui/ # React 19 SPA (Vite + shadcn + Tailwind v4). Built to public/chat/ scripts/ install.sh # Standalone install script for Ubuntu/Debian docker-entrypoint.sh # Docker bootstrap (wait for deps, model pull, init) @@ -124,7 +148,7 @@ docs/ # Documentation (architecture, channels, mcp, security, ## Architecture Overview -Message flow: Slack message -> SlackChannel adapter -> ChannelRouter -> SessionManager (find/create session) -> PromptAssembler (base + role + evolved config + memory context) -> AgentRuntime.query() (Opus 4.6 with full tools) -> response -> ChannelRouter -> Slack thread reply. +Message flow: Slack message -> SlackChannel adapter -> ChannelRouter -> SessionManager (find/create session) -> PromptAssembler (base + role + evolved config + memory context) -> AgentRuntime.query() (Opus 4.6 with full tools) -> response -> ChannelRouter -> Slack thread reply. Web chat uses the same flow: POST /chat/sessions/:id/message -> SSE stream of wire frames -> React client renders in real time. Two separate transcripts (wire-format message store for the client, SDK conversation for the agent) are kept in sync. After each session: EvolutionEngine runs 6-step reflection pipeline -> 5-gate validation -> approved changes applied to phantom-config/ -> version bumped. @@ -193,6 +217,7 @@ Production deployments are managed internally. Do NOT modify production deployme | `src/channels/slack.ts` | Primary channel. Owner access control, threading, reactions. | | `src/mcp/server.ts` | MCP server setup. Tool registration, auth integration. | | `src/memory/system.ts` | Memory coordinator. How the three tiers connect. | +| `src/chat/http.ts` | Web chat backend. SSE streaming, session routing, API handlers. | | `src/core/server.ts` | HTTP server. Routes, health endpoint, version. | | `config/roles/swe.yaml` | SWE role template. Onboarding questions, tools, evolution focus. | | `phantom-config/constitution.md` | Immutable principles the evolution engine cannot modify. | diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8e021c37..014d02d2 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -87,7 +87,7 @@ If you are unsure whether something belongs in TypeScript or in a prompt, open a ## Running Tests ```bash -# Run the full suite (770 tests) +# Run the full suite (1,584 tests) bun test # Run a single test file @@ -101,6 +101,11 @@ bun run lint # Typecheck bun run typecheck + +# Chat UI (separate build, separate package.json) +cd chat-ui && bun install # Install chat-ui dependencies +cd chat-ui && bun run build # Build production SPA +cd chat-ui && bun run typecheck # Type-check the chat client ``` All three must pass before submitting a PR: tests, lint, and typecheck. diff --git a/Dockerfile b/Dockerfile index 81c62e09..5b90b7bd 100644 --- a/Dockerfile +++ b/Dockerfile @@ -124,6 +124,9 @@ RUN mkdir -p /app/data /app/repos && \ # Backup phantom-config defaults so they survive empty volume mount RUN cp -r /app/phantom-config /app/phantom-config-defaults +# Backup image-bundled public assets for entrypoint seeding +RUN cp -r /app/public /app/public-defaults + # Make entrypoint executable RUN chmod +x /app/scripts/docker-entrypoint.sh diff --git a/README.md b/README.md index 0f919390..a915727d 100644 --- a/README.md +++ b/README.md @@ -7,9 +7,9 @@

License - Tests + Tests Docker Pulls - Version + Version

@@ -27,7 +27,7 @@ AI agents today are disposable. You open a chat, get an answer, close the tab, a Phantom takes a different approach: **give the AI its own computer.** A dedicated machine where it installs software, spins up databases, builds dashboards, remembers what you told it last week, and gets measurably better at your job every day. Your laptop stays yours. The agent's workspace is its own. -This is not a chatbot. It is a co-worker that runs on Slack, has its own email address, creates its own tools, and builds infrastructure without asking for permission. Don't take our word for it - scroll down to see what production Phantoms have actually built. +This is not a chatbot. It is a co-worker that runs on Slack, has a web chat interface at `/chat`, has its own email address, creates its own tools, and builds infrastructure without asking for permission. Don't take our word for it - scroll down to see what production Phantoms have actually built. ## What This Actually Looks Like @@ -220,6 +220,7 @@ Because the agent that can only use pre-built tools hits a ceiling. Phantom buil | **Dynamic tools** | Creates and registers its own MCP tools at runtime. Tools survive restarts and work across sessions. | | **Encrypted secrets** | AES-256-GCM encrypted forms with magic-link auth. No plain-text credentials in config files. | | **Email identity** | Every Phantom has its own email address. Send reports to people outside your Slack workspace. | +| **Web chat** | A full browser-based chat client at `/chat` with SSE streaming, file attachments, and Web Push notifications. No Slack required. | | **Shareable pages** | Generates dashboards and tools on a public URL with auth. Share a link, anyone can see it. | | **MCP server** | Claude Code connects to your Phantom. Other Phantoms connect to your Phantom. It is an API, not a dead end. | @@ -238,10 +239,10 @@ Because the agent that can only use pre-built tools hits a ceiling. Phantom buil | | | Channels Agent Runtime | | Slack query() + hooks | -| Telegram Prompt Assembler | -| Email base + role + evolved | -| Webhook + memory context | -| CLI | +| Web Chat Prompt Assembler | +| Telegram base + role + evolved | +| Email + memory context | +| Webhook / CLI | | | | Memory System Self-Evolution Engine | | Qdrant 6-step pipeline | @@ -379,7 +380,7 @@ bun run phantom start ``` ```bash -bun test # 770 tests +bun test # 1584 tests bun run lint # Biome bun run typecheck # tsc --noEmit ``` diff --git a/docker-compose.user.yaml b/docker-compose.user.yaml index 2354d181..fc483264 100644 --- a/docker-compose.user.yaml +++ b/docker-compose.user.yaml @@ -60,6 +60,8 @@ services: ollama: condition: service_started restart: unless-stopped + oom_score_adj: -500 + cpu_shares: 2048 deploy: resources: limits: @@ -69,6 +71,7 @@ services: # consolidation) spawned via runJudgeQuery. The prior 2 GiB cap # cgroup-OOM-killed judge subprocesses under evolution load. memory: 8G + pids: 256 reservations: memory: 512M networks: diff --git a/docs/architecture.md b/docs/architecture.md index 5bedf919..2725ab11 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -58,16 +58,26 @@ Phantom is a single Bun process that runs on a VM. It combines an agent runtime, ### HTTP Server -`src/core/server.ts` - Bun.serve() on port 3100. Three routes: +`src/core/server.ts` - Bun.serve() on port 3100. Key routes: - `/health` - JSON health status (status, uptime, version, channels, memory, evolution) - `/mcp` - MCP Streamable HTTP endpoint - `/webhook` - Inbound webhook receiver +- `/chat/*` - Web chat API (SSE streaming, sessions, attachments, push subscriptions) +- `/ui/*` - Static pages and login (magic link auth) ### Channel Router `src/channels/router.ts` - Multiplexes messages from all connected channels. Each channel implements the `Channel` interface: `connect()`, `disconnect()`, `send()`, `onMessage()`. -Channels: Slack (Socket Mode), Telegram (long polling), Email (IMAP/SMTP), Webhook (HTTP), CLI (readline). +Channels: Slack (Socket Mode), Web Chat (SSE streaming at `/chat`), Telegram (long polling), Email (IMAP/SMTP), Webhook (HTTP), CLI (readline). + +### Web Chat Channel + +`src/chat/` - A full browser-based chat channel with a React 19 SPA at `/chat`. The backend uses Server-Sent Events (SSE) to stream a 32-event wire format from the Agent SDK to the client in real time. Two independent transcripts are maintained: the wire-format message store (what the client sees) and the SDK conversation (what the agent sees). This two-transcript invariant means the client can render markdown, tool calls, thinking blocks, and subagent progress without coupling to SDK internals. + +Auth uses cookie-based sessions with magic link login. On first run without Slack, a login email is sent via Resend (or a bootstrap token is printed to stdout). Web Push notifications (VAPID) alert users when the agent responds while the tab is in the background. + +File attachments (images, PDFs, text files) are uploaded via multipart POST and passed to the agent as context. Type allowlist and size limits are enforced server-side. ### Agent Runtime @@ -114,7 +124,7 @@ Embeddings via Ollama (nomic-embed-text, 768d vectors). Hybrid search using dens ## Data Flow -1. Message arrives via channel (Slack mention, webhook POST, etc.) +1. Message arrives via channel (Slack mention, webhook POST, web chat, etc.) 2. Channel router normalizes to `InboundMessage` 3. Session manager finds or creates a session 4. Prompt assembler builds the full system prompt @@ -123,6 +133,8 @@ Embeddings via Ollama (nomic-embed-text, 768d vectors). Hybrid search using dens 7. Memory consolidation runs (non-blocking) 8. Evolution pipeline runs (non-blocking) +For web chat specifically: the client sends `POST /chat/sessions/:id/message`, the server starts an Agent SDK `query()`, and SDK events are translated to wire frames and pushed to all connected SSE streams for that session (supporting multi-tab). The wire format includes session lifecycle, text streaming, thinking blocks, tool calls with input streaming, and subagent progress. + ## Technology Stack | Component | Technology | @@ -132,7 +144,8 @@ Embeddings via Ollama (nomic-embed-text, 768d vectors). Hybrid search using dens | Vector DB | Qdrant (Docker) | | Embeddings | Ollama (nomic-embed-text) | | State DB | SQLite (Bun built-in) | -| Channels | Slack Bolt, Telegraf, ImapFlow, Nodemailer | +| Channels | Slack Bolt, Web Chat (SSE), Telegraf, ImapFlow, Nodemailer | +| Chat Client | React 19, Vite, shadcn/ui, Tailwind v4 | | Config | YAML + Zod validation | | Process | systemd (on Specter VMs) | @@ -141,6 +154,7 @@ Embeddings via Ollama (nomic-embed-text, 768d vectors). Hybrid search using dens ``` src/ agent/ - Runtime, prompt assembler, hooks, cost tracking + chat/ - Web chat backend (SSE streaming, sessions, attachments, push notifications) channels/ - Slack, Telegram, Email, Webhook, CLI, status reactions cli/ - CLI commands (init, start, doctor, token, status) config/ - YAML config loaders, Zod schemas diff --git a/docs/channels.md b/docs/channels.md index 59f9883b..97ff7c8d 100644 --- a/docs/channels.md +++ b/docs/channels.md @@ -2,9 +2,53 @@ Phantom communicates through pluggable channel adapters. Each channel implements a standard interface, and the agent does not care where messages originate. +## Web Chat + +A browser-based chat interface at `/chat`. No Slack required. This is the simplest way to talk to a Phantom - open the URL in a browser and start typing. + +### Access + +Navigate to `https://your-phantom-host/chat`. On first visit, you will be prompted to log in. + +### Authentication + +Cookie-based sessions with magic link login: + +1. Enter your email address on the login page +2. Phantom sends a magic link via Resend (requires `RESEND_API_KEY` in `.env`) +3. Click the link to authenticate. The session cookie lasts 30 days. + +On first run without Slack configured, Phantom sends a login email to `OWNER_EMAIL` automatically. If Resend is not configured, a bootstrap token is printed to stdout instead. + +### Configuration + +Set these in `.env`: + +``` +OWNER_EMAIL=you@example.com # Required for email-based login +RESEND_API_KEY=re_... # Required for magic link emails +``` + +No channel YAML configuration is needed. The chat channel is always available when the HTTP server is running. + +### Features + +- **SSE streaming** - responses stream token-by-token via Server-Sent Events +- **32-event wire format** - session lifecycle, text, thinking blocks, tool calls with input streaming, subagent progress +- **Multi-tab support** - open the same session in multiple tabs, all stay in sync +- **File attachments** - upload images (JPEG, PNG, GIF, WebP up to 10 MB), PDFs (up to 32 MB), and text/code files (up to 1 MB). Up to 10 files per message. +- **Web Push notifications** - get notified when the agent responds while the tab is in the background. Uses VAPID keys stored in SQLite. +- **Session management** - create, rename, archive, and delete sessions from the sidebar +- **Markdown rendering** - full markdown with code syntax highlighting, tables, and lists +- **Auto-rename** - sessions are automatically titled based on the first exchange + +### Tech Stack + +The chat client is a React 19 SPA built with Vite, shadcn/ui, and Tailwind v4. The production build lives at `public/chat/` and is served as static files. The Dockerfile includes a dedicated build stage for the chat client. + ## Slack -The primary channel. Uses Socket Mode (no public URL required). +Uses Socket Mode (no public URL required). ### Setup (App Manifest) diff --git a/docs/deploy-checklist.md b/docs/deploy-checklist.md index 780bb713..5dee124f 100644 --- a/docs/deploy-checklist.md +++ b/docs/deploy-checklist.md @@ -232,6 +232,20 @@ The `phantom init --yes` step prints MCP tokens. Save the Admin token. This is u } ``` +## Chat UI on Bare Metal Deploys + +Bare metal deployments (rsync-based, not Docker Hub) need to build and overlay the chat client manually after syncing code: + +```bash +# On the VM, after rsync: +cd /home/specter/phantom/chat-ui +bun install --frozen-lockfile +bun run build +cp -r dist/* ../public/chat/ +``` + +Docker Hub deploys get this automatically. The Docker image includes a pre-built chat-ui SPA, and the entrypoint seeding logic copies it into the `phantom_public` volume on every start. No manual overlay is needed. + ## Updating a Deployed Phantom To deploy new code to an existing VM: diff --git a/docs/docker-deploy.md b/docs/docker-deploy.md index b44bb367..8fadf4e7 100644 --- a/docs/docker-deploy.md +++ b/docs/docker-deploy.md @@ -7,10 +7,12 @@ How to deploy Phantom to Specter-provisioned VMs using the Docker Hub image. Thi Specter provisions VMs with Docker, Caddy (TLS), and a placeholder `specter-agent` systemd service. The deploy script replaces the placeholder with the real Phantom running as a Docker container from `ghostwright/phantom` on Docker Hub. No git clone, no `bun install`, no building from source. Three containers run on each VM: -- **phantom** - the AI agent (from Docker Hub) +- **phantom** - the AI agent (from Docker Hub), includes the chat-ui SPA - **phantom-qdrant** - vector memory database - **phantom-ollama** - local embedding model (nomic-embed-text) +The Docker image includes a pre-built React chat client at `/app/public/chat/`. On every container start, the entrypoint seeds image-bundled public assets (chat SPA, base HTML template, examples) into the `phantom_public` volume. This means Docker Hub deploys get the latest chat-ui automatically on `docker compose pull && docker compose up -d` with no manual overlay step. + Caddy reverse-proxies `https://.ghostwright.dev` to `localhost:3100`. This works unchanged because Docker maps port 3100 from the container to the host. ## Prerequisites diff --git a/docs/getting-started.md b/docs/getting-started.md index 872a6aa6..5472d27c 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -8,7 +8,7 @@ This guide walks you through every step. If you get stuck, open an issue on GitH 1. **An Anthropic API key.** Get one at [console.anthropic.com](https://console.anthropic.com/). Starts with `sk-ant-`. 2. **Docker and Docker Compose.** Install from [docs.docker.com/engine/install](https://docs.docker.com/engine/install/). If `docker compose version` prints a version number, you are good. -3. **A Slack workspace** (optional, but recommended). This is how you talk to your Phantom. Any workspace where you can install apps works. +3. **A way to talk to it.** Either a Slack workspace (recommended) or just a browser. The web chat at `/chat` works with no Slack at all - just set `OWNER_EMAIL` in `.env` for login. That is it. No Bun, no Node, no git clone. Docker handles everything. @@ -106,6 +106,16 @@ OWNER_SLACK_USER_ID=U04ABC123XY - `SLACK_APP_TOKEN` - The app-level token you generated (starts with `xapp-`). - `OWNER_SLACK_USER_ID` - Your Slack user ID (starts with `U`). Only this user can talk to Phantom. If you leave this blank, anyone in your workspace can message it. +### Web Chat (no Slack needed) + +``` +OWNER_EMAIL=you@example.com +RESEND_API_KEY=re_... +``` + +- `OWNER_EMAIL` - Your email address. Used for magic link login to the web chat at `/chat`. If Slack is not configured, this is how Phantom authenticates you on first run. +- `RESEND_API_KEY` - API key from [resend.com](https://resend.com). Used to send magic link login emails. If not set, a bootstrap token is printed to container logs instead. + ### Optional ``` @@ -131,6 +141,15 @@ OWNER_SLACK_USER_ID=U04ABC123XY Four lines. That is all Phantom needs. +Without Slack, the minimum is two lines: + +``` +ANTHROPIC_API_KEY=sk-ant-your-key-here +OWNER_EMAIL=you@example.com +``` + +Add `RESEND_API_KEY` for email-based login. Without it, check `docker logs phantom` for the bootstrap token on first start. + ## Step 3: Start Phantom ```bash @@ -177,6 +196,10 @@ curl http://localhost:3100/health You should get a JSON response with `"status":"ok"`. It includes the agent name, Slack connection status, and memory system status. +### Check the web chat + +Open `http://localhost:3100/chat` in your browser. If you set `OWNER_EMAIL`, you will receive a login email (or find a bootstrap token in `docker logs phantom`). After logging in, you can chat with your Phantom directly in the browser. + ### Check Slack If you configured Slack, your Phantom should have sent you a direct message. Open Slack and look for a DM from it. Say hello. It will respond. @@ -403,7 +426,7 @@ Or just ask your Phantom in Slack: "Create an MCP token for Claude Code." It wil ## Next Steps -- [Channels](channels.md) - add Telegram, email, and webhook integrations +- [Channels](channels.md) - web chat, Telegram, email, and webhook integrations - [MCP](mcp.md) - connect external clients and other Phantoms - [Roles](roles.md) - customize your Phantom's specialization - [Self-Evolution](self-evolution.md) - how the agent improves over time diff --git a/docs/memory.md b/docs/memory.md index 70e42e87..e438b802 100644 --- a/docs/memory.md +++ b/docs/memory.md @@ -8,6 +8,8 @@ Phantom has a three-tier vector memory backed by Qdrant and Ollama. Memory persi Query -> Embedding (Ollama) -> Hybrid Search (Qdrant) -> Ranked Results -> Context Builder -> Prompt ``` +All channels share the same memory system. A conversation in the web chat at `/chat` produces the same episodic, semantic, and procedural memories as a Slack conversation. Switching channels does not lose context. + ### Tier 1: Episodic Memory Session transcripts stored as embeddings. Each episode contains: diff --git a/docs/security.md b/docs/security.md index 8eda25b0..5aaf0b25 100644 --- a/docs/security.md +++ b/docs/security.md @@ -47,8 +47,35 @@ These are configured automatically by the [app manifest](../slack-app-manifest.y | `SLACK_APP_TOKEN` | For Slack | App-level token for Socket Mode (`xapp-`) | | `SLACK_CHANNEL_ID` | For Slack | Default channel for intro message on first start | | `TELEGRAM_BOT_TOKEN` | For Telegram | Telegram bot token from @BotFather | +| `OWNER_EMAIL` | For web chat | Email address for magic link login to `/chat` | +| `RESEND_API_KEY` | For email login | Resend API key for sending magic link emails | | `PORT` | No (default 3100) | HTTP server port | +## Web Chat Authentication + +The web chat at `/chat` uses cookie-based sessions with magic link login: + +- On first visit, the user enters their email address +- Phantom sends a one-time magic link via Resend (or prints a bootstrap token to stdout) +- Clicking the link sets a session cookie (30-day expiry, HttpOnly, SameSite=Lax) +- The cookie is validated on every chat API request and SSE connection + +On first run without Slack, Phantom triggers the login flow automatically for `OWNER_EMAIL`. The magic link token is single-use and expires after 30 minutes. + +### Web Push (VAPID) + +Web Push notifications use VAPID (Voluntary Application Server Identification). The VAPID key pair is generated on first use and stored in SQLite (the `kv_store` table). Private keys never leave the server. Push subscriptions are per-browser and stored in SQLite. + +### File Attachment Security + +Uploads to `/chat/sessions/:id/attachments` are validated server-side: + +- **Type allowlist**: images (JPEG, PNG, GIF, WebP), PDF, and text/code files only +- **Size limits**: images 10 MB, PDFs 32 MB, text 1 MB, total request 40 MB +- **Per-request cap**: 10 files maximum +- **Filename sanitization**: path separators, special characters, and leading dots are stripped +- **Preview Content-Disposition**: attachment previews use `inline` disposition so browsers render them rather than downloading + ## MCP Authentication All MCP requests require a Bearer token. Tokens are hashed with SHA-256 before storage. The raw token is shown once during creation and never stored. diff --git a/package.json b/package.json index 460b5240..d8fecec3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "phantom", - "version": "0.18.2", + "version": "0.19.0", "type": "module", "bin": { "phantom": "src/cli/main.ts" diff --git a/scripts/docker-entrypoint.sh b/scripts/docker-entrypoint.sh index b5c5f638..90f0e59a 100755 --- a/scripts/docker-entrypoint.sh +++ b/scripts/docker-entrypoint.sh @@ -49,6 +49,20 @@ if [ -f /app/src/plugins/seed.ts ]; then bun run /app/src/plugins/seed.ts --apply || echo "[phantom] WARNING: plugin seeding failed (continuing)" fi +# Sync image-bundled public assets on every start. +# The phantom_public volume preserves agent-created pages across deploys. +# Image-owned files (chat-ui SPA, base template) must refresh from the new image. +if [ -d /app/public-defaults ]; then + echo "[phantom] Syncing public assets from image..." + cp -r /app/public-defaults/chat /app/public/chat 2>/dev/null || true + for f in _base.html _components.html _agent-name.js index.html phantom-logo.svg; do + [ -f "/app/public-defaults/$f" ] && cp "/app/public-defaults/$f" "/app/public/$f" + done + [ -d /app/public-defaults/_examples ] && cp -r /app/public-defaults/_examples /app/public/_examples + chown -R 999:999 /app/public 2>/dev/null || true + echo "[phantom] Public assets synced" +fi + # Determine service URLs from environment (with Docker Compose defaults) QDRANT_URL="${QDRANT_URL:-http://qdrant:6333}" OLLAMA_URL="${OLLAMA_URL:-http://ollama:11434}" diff --git a/src/chat/types.ts b/src/chat/types.ts index 6dbbd75e..da57be1a 100644 --- a/src/chat/types.ts +++ b/src/chat/types.ts @@ -1,4 +1,4 @@ -// Wire frame types for the 24-event chat streaming protocol. +// Wire frame types for the 32-event chat streaming protocol. // Discriminated union on `event` field. Matches ARCHITECTURE.md Section 2. export type ChatToolState = diff --git a/src/cli/index.ts b/src/cli/index.ts index dd622a70..01950bad 100644 --- a/src/cli/index.ts +++ b/src/cli/index.ts @@ -17,7 +17,7 @@ function printHelp(): void { } function printVersion(): void { - console.log("phantom 0.18.2"); + console.log("phantom 0.19.0"); } export async function runCli(argv: string[]): Promise { diff --git a/src/core/server.ts b/src/core/server.ts index 0d079943..2d49ece4 100644 --- a/src/core/server.ts +++ b/src/core/server.ts @@ -9,7 +9,7 @@ import type { MemoryHealth } from "../memory/types.ts"; import type { SchedulerHealthSummary } from "../scheduler/health.ts"; import { handleUiRequest } from "../ui/serve.ts"; -const VERSION = "0.18.2"; +const VERSION = "0.19.0"; type ChatHandler = (req: Request) => Promise; diff --git a/src/mcp/server.ts b/src/mcp/server.ts index 837ee8e0..f97d0aeb 100644 --- a/src/mcp/server.ts +++ b/src/mcp/server.ts @@ -81,7 +81,7 @@ export class PhantomMcpServer { private createMcpServer(): McpServer { const server = new McpServer( - { name: `phantom-${this.toolDeps.config.name}`, version: "0.18.2" }, + { name: `phantom-${this.toolDeps.config.name}`, version: "0.19.0" }, { capabilities: { logging: {} } }, ); From 2df0db4fd4adb785a5d79eb6b742cba82050d39b Mon Sep 17 00:00:00 2001 From: Muhammad Ahmed Cheema Date: Wed, 15 Apr 2026 20:54:12 -0700 Subject: [PATCH 2/3] fix(docker): rm before cp -r to prevent nested chat/chat/ on restart cp -r source dest creates dest/source/ when dest already exists. On second boot, /app/public/chat/ already exists from the first seeding, so the chat SPA would nest under chat/chat/ and the served files would be stale. Same fix for _examples/. --- scripts/docker-entrypoint.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/docker-entrypoint.sh b/scripts/docker-entrypoint.sh index 90f0e59a..9f2c9751 100755 --- a/scripts/docker-entrypoint.sh +++ b/scripts/docker-entrypoint.sh @@ -54,11 +54,12 @@ fi # Image-owned files (chat-ui SPA, base template) must refresh from the new image. if [ -d /app/public-defaults ]; then echo "[phantom] Syncing public assets from image..." + rm -rf /app/public/chat 2>/dev/null || true cp -r /app/public-defaults/chat /app/public/chat 2>/dev/null || true for f in _base.html _components.html _agent-name.js index.html phantom-logo.svg; do [ -f "/app/public-defaults/$f" ] && cp "/app/public-defaults/$f" "/app/public/$f" done - [ -d /app/public-defaults/_examples ] && cp -r /app/public-defaults/_examples /app/public/_examples + [ -d /app/public-defaults/_examples ] && rm -rf /app/public/_examples && cp -r /app/public-defaults/_examples /app/public/_examples chown -R 999:999 /app/public 2>/dev/null || true echo "[phantom] Public assets synced" fi From 379297f3e2ad2442c4785ed8d5f5e21827b3aa6c Mon Sep 17 00:00:00 2001 From: Muhammad Ahmed Cheema Date: Wed, 15 Apr 2026 21:00:34 -0700 Subject: [PATCH 3/3] fix(docker): pin phantom user UID/GID to 999 The Dockerfile used --system (dynamic allocation) for groupadd/useradd. Currently phantom gets UID 999 because no prior system users exist on oven/bun:1-slim. But any base image update that adds a system user first would shift the UID, breaking the hardcoded chown -R 999:999 calls in the entrypoint. Pinning explicitly with --uid 999 --gid 999 makes the entrypoint chown calls stable regardless of base image changes. --- Dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 5b90b7bd..34606da6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -70,8 +70,8 @@ RUN DPKG_ARCH=$(dpkg --print-architecture) && \ # Claude Code CLI refuses --dangerously-skip-permissions when running as root, # so the container MUST run as a non-root user. Docker socket access is granted # via group_add in docker-compose.yaml (matching the host's docker GID). -RUN groupadd --system phantom && \ - useradd --system --gid phantom --create-home --home-dir /home/phantom phantom && \ +RUN groupadd --system --gid 999 phantom && \ + useradd --system --uid 999 --gid phantom --create-home --home-dir /home/phantom phantom && \ mkdir -p /home/phantom/.claude && \ chown -R phantom:phantom /home/phantom