Skip to content

Commit 75d1f65

Browse files
devartifexCopilotCopilotgithub-advanced-security[bot]
authored
feat: v4.0.0 — session persistence, PWA push, UI overhaul, Azure hardening
* feat: session persistence, push notifications, PWA, security hardening - Session persistence: ChatStateStore persists chat history to disk, cold resume on reconnect - Tab ID switched to localStorage for browser-close survival - Session watcher: fs.watch with debounce for CLI↔browser autosync - PWA: manifest.json, service worker with precaching + push handling - Push notifications: web-push with VAPID, subscription store, server-side push when WS disconnected - Security: auth guards on all endpoints, token revalidation, CSP hardening, CSRF, SSRF protection - Azure: VNet with 3 subnets, Key Vault, Premium ACR, storage hardening, diagnostics - Docker: new data dirs, VAPID env vars, volume mount updates Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * docs: update architecture, security, readme, copilot-instructions post-implementation Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: lazy-init singletons to avoid config access at build time push-singleton and chat-state-singleton were eagerly calling config.* at module import time, causing npm run build to fail with 'Missing required env var: SESSION_SECRET' during SvelteKit's post-build analysis step. Fixed by wrapping both singletons in Proxy with lazy initialization — config is only accessed on first actual method call at runtime. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * chore: add VAPID and data path vars to .env.example Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * feat: update app logo with new Copilot Unleashed SVG branding - Add new logo SVG (logo-copilot unleashed.svg → logo.svg) with dark gradient brackets, spark effect, and sparkle stars on #09090b background - Regenerate all PNG icons (favicon, 192, 512, maskable) from new SVG - Update TopBar.svelte and DeviceFlowLogin.svelte: /img/logo.png → /img/logo.svg - Update app.html: add SVG favicon link (rel=icon svg+xml) + apple-touch-icon uses icon-192 - Update manifest.json: add SVG icon entry (sizes=any) as first icon - Update generate-pwa-icons.mjs: use logo.svg as source instead of favicon.png Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: use logo without background for in-app display, add favicon.svg - Create logo-no-bg.svg: same graphics as logo.svg but no dark background rect or border — transparent background for use in header and login page - TopBar.svelte + DeviceFlowLogin.svelte: switch to logo-no-bg.svg - Add static/favicon.svg (full icon with background) for browser favicon - app.html: point SVG favicon link to /favicon.svg (root-level, conventional) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * chore: remove old logo files - Remove original 'logo-copilot unleashed.svg' (now logo.svg/logo-no-bg.svg) - Remove logo.png (replaced by SVG logos) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: restore chat history on warm reconnect and persist during resume Two bugs that caused conversation history to not show after browser reopen: 1. Warm reconnect (pool entry still alive within TTL window) sent session_reconnected without any message history. Now loads persisted state and sends cold_resume so the client repopulates messages. 2. wireSessionEvents in resume-session.ts was called without userLogin and tabId, so assistant messages from resumed sessions were never persisted to ChatStateStore. Now passes both parameters. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: support concurrent permission prompts and fix file attachment z-index Bug 1 — Permission queue: Previously, every layer used a single-slot for permission requests. When the SDK fired multiple concurrent permission requests, each new one overwrote the previous — losing its resolve callback forever. Now uses Map<requestId, resolver> at every layer: - PoolEntry: permissionResolves Map + pendingPermissionPrompts Map - makePermissionHandler: adds to map instead of overwriting - handlePermissionResponse: looks up by requestId from client - Reconnect: re-sends ALL pending permission prompts - Client chat store: pendingPermissions array (was single | null) - Page template: {#each} over all pending permissions Bug 2 — File attachment preview clipped: The .file-preview-row was inside .input-container which has overflow: hidden (for border-radius clipping). Moved the file preview row outside and above .input-container so it renders in the normal .input-area flow without being clipped. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * feat(ui): align user messages right, Copilot left for visual distinction User messages now appear right-aligned with border-right, mirroring universal chat UX (WhatsApp/iMessage). Assistant messages remain left-aligned with border-left. No color change — layout alone communicates sender identity clearly. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(sessions): filter empty SDK sessions from list, strip /home/node cwd The Copilot SDK creates a session entry for every connection, including ones where no messages were ever sent. These appear in the session list as empty entries labelled '/home/node'. Fix: after merging SDK + filesystem sessions, discard entries with no title, no checkpoints, no plan, and a cwd that is just the container home dir. Also strip cwd='/home/node' from enriched metadata so it never surfaces in the UI. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(sessions): resume existing SDK session on page refresh instead of creating new one Previously, every browser refresh sent new_session unconditionally, creating a blank SDK session even when a previous session existed. This caused the session list to fill up with empty '/home/node' entries. New flow on 'connected': - Wait up to 400ms for a cold_resume message from the server - If cold_resume arrives with sdkSessionId → resume_session (no new session created) - If cold_resume has no sdkSessionId (history but no live SDK session) → new_session - If timeout fires (no cold_resume) → new_session as before Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * perf(sessions): eliminate 400ms delay — include sdkSessionId in connected message Server now pre-loads persisted chat state before sending 'connected', embedding sdkSessionId and hasPersistedState directly in the message. Client decides instantly on receipt: resume_session if sdkSessionId exists, requestNewSession otherwise. No timer, no delay, no race. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * feat(ui): loading animation during session restore, eliminate flash Show a pulsing logo + 'Restoring session…' text while the server loads persisted state and the SDK session resumes. The empty chat (Banner) is never shown during restore — only after confirmation that there is no previous session. sessionLoading starts true and clears on cold_resume / session_created / session_resumed. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * feat(deployer): add deployer IP address parameter for ACR access control * fix(ui): use skeleton shimmer for session loading, fix stuck state Replace custom logo pulse animation with the app's standard skeleton shimmer bars (same pattern as SessionsSheet loading). Fixes: - sessionLoading now also clears on session_reconnected - If resume fails (error message while loading), auto-fallback to requestNewSession() instead of staying stuck forever Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * feat(ui): redesign Banner and TopBar following ChatGPT/Claude patterns Banner (empty chat): - Logo centered + 'Hello, {username}' greeting (like Claude/Gemini) - Simplified hints on one line, no version number - Clean, focused, inviting TopBar: - Shows 'New chat' text when no session title (no redundant logo) - Session title shown when active session exists - Removed brand-group/logo/gradient from top bar (logo lives in Banner) Login page: unchanged (already well-designed) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * chore: bump version to 4.0.0 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(infra): wire deployerIpAddress to Bicep params, auto-detect IP preprovision - Add deployerIpAddress to main.parameters.json so azd passes it to Bicep - Add preprovision hook to auto-detect current public IP via ipify.org - This ensures ACR IP allowlist is always set to the deployer's current IP Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * feat(ui): enhance attachment menu positioning and visibility in ChatInput component * feat(ui): add brand group to TopBar for improved visibility when no session title * docs: consolidate duplicate feature entries in README Merge 'Persistent sessions' + 'Session persistence' into one entry. Merge 'CLI ↔ Browser sync' + 'CLI ↔ Browser autosync' into one entry. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * chore: remove broken dev:local script and all references The dev:local script bypassed server.js (express-session + WebSocket), so it never worked properly. Keep only npm run dev (Docker Compose). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * feat(ui): remove New Chat button from TopBar (keep in Sidebar only) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Update infra/modules/storage.bicep Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Potential fix for code scanning alert no. 10: Clear-text logging of sensitive information Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> * refactor(infra): lean architecture — drop VNet, scale to zero, ACR Basic BREAKING: Complete infrastructure overhaul for cost/simplicity: Removed (900+ lines deleted): - VNet + 3 subnets + 3 NSGs + 3 private endpoints + 3 DNS zones - App Insights + diagnostics module - Storage account (subscription policy blocks shared key access) - ACR Premium → Basic ($5/mo vs $50/mo) - abbreviations.json + container-apps.json ARM template Simplified: - Key Vault: RBAC-only (no network restrictions, no PE) - ACR: Basic SKU, deployer IP allowlist (no PE) - Container Apps: no VNet, scale-to-zero (minReplicas: 0), EmptyDir volume - Monitoring: Log Analytics only (no App Insights) - Naming: readable cu-{env} pattern with 4-char hash for global uniqueness Volume: EmptyDir (ephemeral) — subscription Azure Policy blocks allowSharedKeyAccess on storage accounts, preventing SMB mounts. Data at /data persists across container restarts but not scaling events. Cost: ~$85-90/mo → ~$10-20/mo (75-85% savings) Security: RBAC + managed identity (same protection, less complexity) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Potential fix for code scanning alert no. 14: Clear-text logging of sensitive information Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> * fix(security): resolve GHAS findings — ReDoS regex + CI permissions - file-mentions.ts: Replace nested quantifier regex with flat character class to eliminate catastrophic backtracking (GHAS #9, high severity) - ci.yml: Add explicit permissions (contents: read) to all workflow jobs following least-privilege principle (GHAS #5, #6, #7, medium severity) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
1 parent 58d2788 commit 75d1f65

89 files changed

Lines changed: 4979 additions & 374 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.env.example

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,15 @@ SESSION_SECRET=
1111
# Set to share session state with the Copilot CLI.
1212
# Example: ~/.copilot (matches the CLI's default config directory)
1313
# COPILOT_CONFIG_DIR=~/.copilot
14+
15+
# === Push Notifications (optional) ===
16+
# Generate keys with: node scripts/generate-vapid-keys.mjs
17+
# iOS push requires the app to be installed as a PWA (Add to Home Screen in Safari).
18+
VAPID_PUBLIC_KEY=
19+
VAPID_PRIVATE_KEY=
20+
VAPID_SUBJECT=mailto:admin@example.com
21+
22+
# === Custom Data Paths (optional, defaults shown) ===
23+
# Override where chat history and push subscriptions are stored.
24+
# CHAT_STATE_PATH=.chat-state
25+
# PUSH_STORE_PATH=.push-subscriptions

.github/copilot-instructions.md

Lines changed: 57 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ Self-hosted multi-model AI chat platform powered by the official `@github/copilo
1717
- **Settings**: Persisted server-side via `/api/settings` and mirrored in `localStorage`
1818
- **Deployment**: Docker container → Azure Container Apps via `azd up`
1919

20+
### Deployment
21+
- **Azure**: VNet with subnets, Key Vault for secrets, Premium ACR, NFS volume with private endpoints; `COPILOT_CONFIG_DIR=/data/copilot-home`
22+
- **Local Docker**: Bind mount `~/.copilot` preserved for CLI sync; `COPILOT_CONFIG_DIR` stays at `/home/node/.copilot`
23+
2024
## Tech Stack
2125

2226
| Layer | Technology |
@@ -46,22 +50,53 @@ svelte.config.js # SvelteKit config (adapter-node)
4650
vite.config.ts # Vite config
4751
vitest.config.ts # Vitest config for colocated unit tests
4852
53+
scripts/
54+
└── generate-vapid-keys.mjs # VAPID key generation utility for push notifications
55+
static/
56+
├── manifest.json # PWA manifest (name, icons, start_url, display: standalone)
57+
└── sw.js # Service worker: precaching, push handler, notification click
58+
infra/
59+
├── modules/
60+
│ ├── vnet.bicep # Azure VNet with subnets for private endpoints
61+
│ └── key-vault.bicep # Azure Key Vault for secrets management
62+
4963
src/
5064
├── app.html # SvelteKit shell (viewport, theme-color, PWA meta)
5165
├── app.css # Global reset, design tokens, highlight.js theme
52-
├── hooks.server.ts # Session bridge, CSP headers, rate limiting
66+
├── hooks.server.ts # Session bridge, CSP headers, rate limiting, CSRF, token revalidation
5367
├── hooks.server.test.ts # Example colocated unit test pattern
5468
5569
├── lib/
5670
│ ├── components/ # 20 Svelte 5 components (see ARCHITECTURE.md)
5771
│ ├── stores/ # 4 rune stores: auth, chat, settings, ws
58-
│ ├── server/ # 22 server files: auth, copilot, settings, skills, ws, security
72+
│ ├── server/ # Server-only code
73+
│ │ ├── auth/
74+
│ │ │ ├── github.ts # Device Flow OAuth (fetch-based)
75+
│ │ │ └── guard.ts # Auth middleware + 30-min token revalidation
76+
│ │ ├── copilot/
77+
│ │ │ ├── client.ts # CopilotClient factory
78+
│ │ │ └── session.ts # Session config (model, reasoning, tools, hooks)
79+
│ │ ├── ws/
80+
│ │ │ ├── handler.ts # WebSocket handler: 22+ message types, SDK events
81+
│ │ │ └── session-pool.ts # Per-user session pool with TTL + buffer
82+
│ │ ├── push/
83+
│ │ │ ├── subscription-store.ts # Push subscription CRUD per user (10 sub cap)
84+
│ │ │ └── sender.ts # web-push wrapper with 60-min TTL, auto-cleanup
85+
│ │ ├── chat-state-store.ts # Persistent chat state per user+tab (1000-msg cap, atomic writes)
86+
│ │ ├── session-watcher.ts # fs.watch on session-state dir, 100ms debounce
87+
│ │ ├── init.ts # Server-side initialization (watcher + signal handlers)
88+
│ │ ├── config.ts # Env var validation (fail-fast)
89+
│ │ ├── security-log.ts # Structured security event logging
90+
│ │ └── session-store.ts # Express session ↔ SvelteKit bridge
5991
│ ├── types/index.ts # All message types: 60 server + 18 client = 78 total
60-
│ └── utils/markdown.ts # Shared markdown pipeline
92+
│ └── utils/
93+
│ ├── markdown.ts # Shared markdown pipeline
94+
│ ├── push-client.ts # Client-side push subscription management
95+
│ └── sw-register.ts # Service worker registration
6196
6297
├── routes/
6398
│ ├── +page.svelte # Main page: login or full chat screen (1 page route)
64-
│ ├── +layout.svelte # App shell layout
99+
│ ├── +layout.svelte # App shell layout (registers service worker)
65100
│ ├── +layout.server.ts # Root auth/session bootstrap
66101
│ ├── +error.svelte # Route error boundary
67102
│ ├── auth/
@@ -76,8 +111,11 @@ src/
76111
│ │ ├── skills/+server.ts
77112
│ │ ├── upload/+server.ts
78113
│ │ ├── version/+server.ts
79-
│ │ └── sessions/sync/+server.ts
80-
│ └── health/+server.ts # 12 +server.ts endpoint routes total
114+
│ │ ├── sessions/sync/+server.ts
115+
│ │ ├── push/subscribe/+server.ts # POST: register push subscription
116+
│ │ ├── push/unsubscribe/+server.ts # POST: remove push subscription
117+
│ │ └── push/vapid-key/+server.ts # GET: public VAPID key
118+
│ └── health/+server.ts # 18 +server.ts endpoint routes total
81119
82120
└── **/*.test.ts # Vitest unit tests live next to source files
83121
```
@@ -105,6 +143,10 @@ src/
105143
- **Try-catch** in route handlers and WebSocket — return JSON errors to client
106144
- **Message type whitelist** — WebSocket handler validates against `VALID_MESSAGE_TYPES` Set
107145
- **Session disconnect** — use `session.disconnect()` (not deprecated `destroy()`)
146+
- **Atomic file writes** — ChatStateStore (and settings-store) writes to `.tmp` then renames, preventing partial reads on crash
147+
- **Session watcher**`fs.watch` on session-state directory with 100ms debounce, broadcasts state changes to all connected WS clients
148+
- **Push on disconnect** — Push notifications triggered in `poolSend()` when the target user's WS is disconnected
149+
- **Cold resume** — Load persisted chat state from disk, resume SDK session via `client.resumeSession()`, replay messages to the client
108150

109151
### Security
110152
- CSP in hooks.server.ts: self + unsafe-inline (Svelte) + ws/wss + GitHub avatars
@@ -113,8 +155,11 @@ src/
113155
- XSS prevention: DOMPurify sanitizes all rendered markdown
114156
- Max message length: 10,000 chars (server-enforced)
115157
- Upload limits: 10MB per file, 5 files max, extension allowlist
116-
- SSRF prevention: internal IP range blocklist for custom webhook tools
158+
- SSRF prevention: internal IP range blocklist for custom webhook tools; `redirect:'manual'` on all outbound fetches
117159
- Token revalidation on WebSocket connect (catches revoked tokens)
160+
- All API endpoints require `checkAuth()` except `/health`
161+
- CSRF: Origin header required on mutation requests in production
162+
- Periodic token revalidation every 30 minutes (server-side interval)
118163

119164
### Environment Variables
120165
| Variable | Required | Default | Purpose |
@@ -126,6 +171,11 @@ src/
126171
| `NODE_ENV` | No | development | Dev vs prod behavior |
127172
| `ALLOWED_GITHUB_USERS` | No || Comma-separated GitHub usernames |
128173
| `TOKEN_MAX_AGE_MS` | No | 86400000 | Force re-auth interval (24h) |
174+
| `CHAT_STATE_PATH` | No | ./data/chat-state | Directory for persisted chat state files |
175+
| `VAPID_PUBLIC_KEY` | No || VAPID public key for web push (generate with `scripts/generate-vapid-keys.mjs`) |
176+
| `VAPID_PRIVATE_KEY` | No || VAPID private key for web push |
177+
| `VAPID_SUBJECT` | No || VAPID subject (mailto: or https: URL) |
178+
| `PUSH_STORE_PATH` | No | ./data/push-subscriptions | Directory for push subscription storage |
129179

130180
## Build & Run
131181

@@ -138,7 +188,6 @@ npm install && npm run build && npm start
138188

139189
# Development
140190
npm run dev # Docker Compose development stack
141-
npm run dev:local # Local Vite dev server after building server-side TypeScript
142191

143192
# Type check
144193
npm run check # svelte-check

.github/workflows/ci.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ on:
66
pull_request:
77
branches: [main, master]
88

9+
permissions:
10+
contents: read
11+
912
concurrency:
1013
group: ci-${{ github.ref }}
1114
cancel-in-progress: true

CONTRIBUTING.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,12 @@ Thank you for your interest in contributing! This project is a self-hosted, mult
77
1. Fork the repo and clone it
88
2. Install dependencies: `npm install`
99
3. Create a `.env` file (see README for required variables)
10-
4. Run in development mode: `npm run dev:local`
10+
4. Run in development mode: `npm run dev`
1111

1212
## Development
1313

1414
| Command | Purpose |
1515
|---------|---------|
16-
| `npm run dev:local` | Start Vite dev server |
1716
| `npm run dev` | Start via Docker Compose |
1817
| `npm run build` | Production build |
1918
| `npm run check` | Type check with svelte-check |

Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ ENV PORT=3000
2525
ENV HOME=/home/node
2626

2727
RUN npm install -g @github/copilot
28-
RUN mkdir -p /home/node/.copilot/session-state /data/sessions /data/settings && chown -R node:node /home/node /data
28+
RUN mkdir -p /home/node/.copilot/session-state /data/sessions /data/settings /data/chat-state /data/push-subscriptions /data/copilot-home && chown -R node:node /home/node /data
2929

3030
# Copy bundled CLI session data if it was prepared with scripts/bundle-sessions.mjs
3131
COPY --from=builder --chown=node:node /tmp/copilot-config/ /home/node/.copilot/

README.md

Lines changed: 120 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,9 @@ Your Copilot subscription already gives you access to Claude Opus 4.6, GPT-5.4,
5151
- **Image vision** — attach images alongside code and documents; vision-capable models analyze them inline
5252
- **File & directory attachments** — drop in code files, images, CSVs, or whole directories with `@` mention autocomplete
5353
- **Issue & PR references** — type `#` to search and reference GitHub issues/PRs across all your repos
54-
- **Persistent sessions** — resume any conversation, on any device, with full checkpoint history
55-
- **CLI ↔ Browser sync** — sessions started in the Copilot CLI appear in the browser and vice versa
54+
- **Persistent sessions** — resume any conversation on any device with full checkpoint history; chat state survives browser close via server-side storage with cold resume from disk
55+
- **CLI ↔ Browser sync** — sessions started in the Copilot CLI appear in the browser and vice versa, with automatic filesystem watcher and Docker bind mount for real-time sync
56+
- **Push notifications** — Web Push alerts when the browser is closed; triggers on response ready, errors, permission prompts, and user input; full PWA support (iOS 16.4+ requires "Add to Home Screen" from Safari)
5657
- **Plan mode** — agent creates an editable execution plan before acting; bidirectional sync with `plan.md` on disk
5758
- **Fleet mode** — launch multi-agent parallel execution with per-agent status tracking
5859
- **Quota tracking** — see premium request usage, remaining balance, and reset date at a glance
@@ -143,7 +144,81 @@ Open [localhost:3000](http://localhost:3000). Log in with GitHub. Done.
143144
azd up
144145
```
145146

146-
That's it. Container Apps, ACR, managed identity, TLS, monitoring — all provisioned automatically.
147+
That's it. Container Apps, ACR, Key Vault, managed identity, VNet, TLS, and monitoring — all provisioned automatically.
148+
149+
### Required parameters
150+
151+
`azd up` will prompt for these if not already set:
152+
153+
| Parameter | How to set | Notes |
154+
|-----------|-----------|-------|
155+
| `GITHUB_CLIENT_ID` | `azd env set GITHUB_CLIENT_ID <id>` | Your GitHub OAuth App client ID |
156+
| `SESSION_SECRET` | auto-generated as `newGuid()` if omitted | 32+ char random string |
157+
158+
### Deployer IP (required for `azd deploy` with private ACR)
159+
160+
The default infrastructure uses a **Premium ACR with public network access disabled** and a private endpoint. This blocks `azd deploy` pushes from your local machine. Set your current public IP before deploying:
161+
162+
```bash
163+
azd env set DEPLOYER_IP_ADDRESS "$(curl -s https://api.ipify.org)"
164+
azd up
165+
```
166+
167+
The ACR firewall will allow only that IP (`defaultAction: Deny`, single `Allow` rule). All other public access remains blocked. For CI/CD pipelines, set `DEPLOYER_IP_ADDRESS` to the runner's outbound IP in the same way.
168+
169+
### Optional: VAPID keys for push notifications
170+
171+
```bash
172+
node scripts/generate-vapid-keys.mjs
173+
azd env set VAPID_PUBLIC_KEY "<key>"
174+
azd env set VAPID_PRIVATE_KEY "<key>"
175+
azd env set VAPID_SUBJECT "mailto:you@example.com"
176+
```
177+
178+
### Bootstrapping secrets in Key Vault
179+
180+
`azd provision` stores `GITHUB_CLIENT_ID`, `SESSION_SECRET`, and (optionally) VAPID keys as secrets in Azure Key Vault. The Container App reads them at runtime via the managed identity — no secrets in environment variables or container image.
181+
182+
### Troubleshooting `azd up`
183+
184+
<details>
185+
<summary>Container App fails with MANIFEST_UNKNOWN image tag</summary>
186+
187+
If provisioning fails mid-run, the `.azure/<env>/.env` may hold a stale `SERVICE_WEB_IMAGE_NAME` pointing to a tag that no longer exists in a newly-recreated ACR. Clear it before retrying:
188+
189+
```bash
190+
azd env set SERVICE_WEB_IMAGE_NAME ""
191+
azd up
192+
```
193+
194+
`azd provision` will deploy with the placeholder image; `azd deploy` pushes the real image immediately after.
195+
196+
</details>
197+
198+
<details>
199+
<summary>ACR push fails with 403 Forbidden / IP not allowed</summary>
200+
201+
Set your public IP and re-provision to open the ACR firewall:
202+
203+
```bash
204+
azd env set DEPLOYER_IP_ADDRESS "$(curl -s https://api.ipify.org)"
205+
azd provision # updates ACR network rules
206+
azd deploy # push succeeds
207+
```
208+
209+
</details>
210+
211+
<details>
212+
<summary>Container App fails: unable to fetch secret from Key Vault</summary>
213+
214+
This means the Key Vault secret doesn't exist yet (typically on a fresh deploy). Ensure `GITHUB_CLIENT_ID` is set and run `azd provision` again — it will create the secrets in Key Vault as part of infra creation.
215+
216+
```bash
217+
azd env set GITHUB_CLIENT_ID "<your-id>"
218+
azd provision
219+
```
220+
221+
</details>
147222

148223
---
149224

@@ -170,16 +245,29 @@ That's it. Container Apps, ACR, managed identity, TLS, monitoring — all provis
170245
| `SESSION_STORE_PATH` | `/data/sessions` | Persistent session directory |
171246
| `SETTINGS_STORE_PATH` | `/data/settings` | Per-user settings directory |
172247
| `COPILOT_CONFIG_DIR` | `~/.copilot` | Copilot session-state directory (share with CLI for bidirectional sync) |
248+
| `CHAT_STATE_PATH` | `.chat-state` (dev) / `/data/chat-state` (prod) | Persisted chat state storage |
249+
| `VAPID_PUBLIC_KEY` || VAPID public key (base64url) — required for push notifications |
250+
| `VAPID_PRIVATE_KEY` || VAPID private key (base64url) — required for push notifications |
251+
| `VAPID_SUBJECT` | `mailto:admin@example.com` | VAPID subject identifier |
252+
| `PUSH_STORE_PATH` | `/data/push-subscriptions` | Push subscription storage path |
173253

174254
</details>
175255

256+
### Generate VAPID keys (for push notifications)
257+
258+
```bash
259+
node scripts/generate-vapid-keys.mjs
260+
```
261+
262+
Copy the output into your `.env` file. Push notifications require all three VAPID variables (`VAPID_PUBLIC_KEY`, `VAPID_PRIVATE_KEY`, `VAPID_SUBJECT`).
263+
176264
---
177265

178266
## CLI ↔ Browser session sync
179267

180268
Copilot Unleashed and the GitHub Copilot CLI share the same session-state directory (`~/.copilot/session-state/`). By default, the app reads from the same location the CLI uses — so any session started in the terminal is available in the browser the moment you open the Sessions panel.
181269

182-
> **Note:** When running via Docker (`npm run dev`), the `docker-compose.yml` mounts `~/.copilot` read-only into the container. If you use `npm run dev:local` (no Docker), the app reads directly from your host `~/.copilot` with no extra config needed.
270+
> **Note:** When running via Docker (`npm run dev`), the `docker-compose.yml` mounts `~/.copilot` read-only into the container.
183271
184272
### How it works
185273

@@ -303,11 +391,39 @@ Scopes: `copilot` (API access) + `read:user` (avatar) + `repo` (SDK tools need i
303391
- Structured security event logging
304392
- Optional user allowlist via `ALLOWED_GITHUB_USERS`
305393
- CodeQL scanning + secret scanning via GitHub Advanced Security
394+
- All API endpoints require GitHub authentication — no anonymous access
395+
- Periodic token revalidation ensures revoked tokens are caught promptly
396+
- **Azure**: VNet isolation for Container Apps, Key Vault for secrets management, Premium ACR with private endpoints
306397
307398
</details>
308399
309400
---
310401
402+
## Data Persistence
403+
404+
All stateful data survives container restarts when volumes are configured correctly.
405+
406+
| Data | Path (default) | Survives restart? |
407+
|------|----------------|:-:|
408+
| SDK sessions & checkpoints | `COPILOT_CONFIG_DIR` (`~/.copilot`) | ✅ with volume |
409+
| App session metadata | `SESSION_STORE_PATH` (`/data/sessions`) | ✅ with volume |
410+
| Per-user settings | `SETTINGS_STORE_PATH` (`/data/settings`) | ✅ with volume |
411+
| Chat history | `CHAT_STATE_PATH` (`/data/chat-state`) | ✅ with volume |
412+
| Push subscriptions | `PUSH_STORE_PATH` (`/data/push-subscriptions`) | ✅ with volume |
413+
414+
**Local Docker:** Use a named volume for `/data` and a bind mount for `~/.copilot` (enables CLI ↔ Browser sync).
415+
416+
```yaml
417+
# docker-compose.yml
418+
volumes:
419+
- app-data:/data # sessions, settings, chat state, push subs
420+
- ~/.copilot:/home/node/.copilot # SDK session-state (shared with CLI)
421+
```
422+
423+
**Azure Container Apps:** Use an NFS-backed Azure Files mount at `/data`. The Bicep infrastructure provisions this automatically via `azd up`.
424+
425+
---
426+
311427
## Built with
312428

313429
SvelteKit 5 · Svelte 5 runes · TypeScript 5.7 · Node.js 24 · `@github/copilot-sdk` · Vite · `ws` · Vitest · Playwright · Docker · Bicep

0 commit comments

Comments
 (0)