Skip to content
This repository was archived by the owner on Jun 8, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ jobs:
- name: Generate build info
run: pnpm -r prebuild

- name: Lint
run: pnpm -C apps/desktop lint

- name: Typecheck
run: pnpm typecheck

Expand Down
28 changes: 28 additions & 0 deletions .gitmessage
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Git Commit Message Template
#
# Format:
# <TICKET-NUMBER>: <Short description (imperative mood, present tense)>
# (TICKET-NUMBER extracted from branch name)
#
# <Long description (bullet points explaining what changed)>
#
# Testing: <Manual testing performed>
# Risks: <Risk assessment of changes>
#
# Guidelines:
# - Short description: TICKET-NUMBER: description (TICKET-NUMBER from branch name), max 50 chars total, no period, imperative mood
# - Long description: bullet points with dashes, wrap at 72 chars
# - Testing: describe manual verification steps taken
# - Risks: identify deployment/production risks or "None identified" if adequately mitigated
# - Ticket: use current branch name (e.g., CHC-1234)
#
# Example (assuming branch name is CHC-4394):
# CHC-4394: Improve performance logging
#
# - Reduced performance logging to single logger.adebug call
# - Consolidated metrics into single dictionary with elapsed_time
# - Created perf_monitor.py to simplify performance monitoring
#
# Testing: Ran locally to verify no crashes and logs appear with debug enabled
#
# Risks: None identified
144 changes: 13 additions & 131 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -1,138 +1,20 @@
# ClosedLoop Electron - Development Notes
# ClosedLoop Electron Monorepo

## Testing the Local Gateway (HTTP API)
## Structure

The desktop Electron app runs a localhost HTTP gateway. To test it manually:
- `apps/desktop/` -- Electron desktop app (localhost HTTP gateway, cloud relay, tray UI)

### Authentication
## Useful Commands

Engineer routes (`/api/engineer/*`) require one of:
1. **Internal gateway token** (`X-Desktop-Gateway-Token`) — used by cloud command executor for internal calls.
2. **Browser session token** (`X-Desktop-Session-Token`) — obtained via authenticated challenge-exchange flow. Must be accompanied by an `Origin` header matching the origin bound during exchange.
Run `just` to see all available recipes. Key ones:

**Origin-only auth is not supported.** A spoofed `Origin` header alone will not grant access to engineer routes.
- `just desktop-dev` -- build and start Electron
- `just desktop-no-auth` -- start with gateway auth disabled (dev only)
- `just desktop-debug-auth` -- start with debug token minting enabled
- `just desktop-lint` -- run ESLint
- `just desktop-typecheck` -- run TypeScript type checking
- `just desktop-test` -- run tests

### Fail-Closed Behavior (Missing API Key)
## Commit Messages

The gateway **fails closed** when the desktop API key is not configured:
- **App startup is unaffected** — the server binds, the UI opens, health endpoint works, cloud relay works.
- **Local-electron browser mode becomes unavailable** — the challenge-exchange route returns HTTP 503 `"Local gateway auth unavailable: API key required"`. Without a session token, all engineer routes return 401.
- **No silent fallback** — the browser interceptor surfaces the 503 error to the UI rather than silently degrading or falling back to an insecure auth path.
- **No crashes** — `getApiKey()` returns `null` safely; no uncaught exceptions.

"Fail closed" means the *feature* is unavailable with an explicit, actionable error — not that the app crashes.

The health endpoint remains unauthenticated:
```bash
curl -s http://localhost:<PORT>/health
```

The gateway port is visible in the health response or the Electron UI. Typical dev port: `19432`.

### Debug Auth for Development

For manual `curl` testing during development, use the debug auth workflow:

1. Start Electron with debug auth enabled: `just desktop-debug-auth`
2. In the Electron UI Settings panel, click **Mint Debug Token** to generate a short-lived session token.
3. Use the token in curl:

```bash
curl -s \
-H "X-Desktop-Session-Token: <token>" \
-H "Origin: http://localhost" \
"http://localhost:19432/api/engineer/directories?path=/Users/<you>/Source"
```

Debug tokens are short-lived (10 minutes), memory-only, and only available when `CL_LOCAL_GATEWAY_DEBUG_AUTH=1` in an unpackaged build.

### Sandbox Directory Enforcement

Every API route checks target paths against the sandbox base directory via `isPathAllowed()` in `src/server/security.ts`. Paths outside the sandbox return HTTP 403 `{"error": "directory not allowed"}`.

The `sandboxBaseDirectory` is the single source of truth for path access. It is stored in `electron-store` via `SettingsStore` and used to derive a single-entry allowlist at runtime via `buildAllowedDirectories()` in `src/shared/sandbox-policy.ts`.

Hardcoded sensitive paths (`~/.ssh`, `~/.gnupg`, `~/.aws`, `~/Library/Keychains`, `/etc`, `/bin`, `/sbin`) are always denied, even if a parent is allowed.

### Example Test Commands

Obtain a debug token first via `just desktop-debug-auth` + UI "Mint Debug Token" button, then:

```bash
# Should succeed (with valid session token, sandbox directory)
curl -s -H "X-Desktop-Session-Token: <TOKEN>" -H "Origin: http://localhost" \
"http://localhost:19432/api/engineer/directories?path=/Users/<you>/Source"

# Should fail — no session token (401)
curl -s -H "Origin: http://localhost" "http://localhost:19432/api/engineer/directories?path=/Users/<you>/Source"

# Should fail — outside sandbox (403)
curl -s -H "X-Desktop-Session-Token: <TOKEN>" -H "Origin: http://localhost" \
"http://localhost:19432/api/engineer/directories?path=/tmp"

# Should fail — sensitive deny list (403)
curl -s -H "X-Desktop-Session-Token: <TOKEN>" -H "Origin: http://localhost" \
"http://localhost:19432/api/engineer/directories?path=/Users/<you>/.ssh"
```

## Releasing Desktop Builds

Releases are automated via CI. When a PR that touches `apps/desktop/**` is merged to main, the release workflow runs:

1. Reads the `version` from `apps/desktop/package.json`
2. Checks if that version already has a GitHub Release — if so, skips with a warning
3. Builds a universal macOS DMG via `electron-builder`
4. Publishes the DMG to GitHub Releases and uploads it as a workflow artifact
5. Sends a Slack notification to the team

### Triggering a new release

Bump the `version` field in `apps/desktop/package.json` as part of your PR. When it merges, CI will build and publish automatically.

```jsonc
// apps/desktop/package.json
{ "version": "0.2.0" } // ← bump this
```

If you merge desktop changes **without** bumping the version, the workflow will skip the build and log a warning — no harm done, no duplicate releases.

### Version Bump Rule

**Any commit that touches files in `apps/desktop/` MUST include a version bump in `apps/desktop/package.json`.** Before committing, check whether `package.json` is already modified in the staged changes. If the version was already bumped (e.g. by a prior edit in the same branch), do not bump again. If it was not bumped, increment the patch version (e.g. `0.4.0` -> `0.4.1`) and stage it alongside the other changes. A CI check will fail the PR if desktop files changed without a version bump.

### Auto-update for users

- **Packaged builds** (DMG installs) use `electron-updater` to check GitHub Releases every 5 minutes. Users are notified in-app when a new version is available, and it auto-installs on quit.
- **Dev builds** (running from source) compare `origin/main` commit hashes via `git fetch` and offer to pull + rebuild.

### Required GitHub secrets

- `SLACK_GITHUB_REPO_WEBHOOK_URL` — Slack incoming webhook for release notifications

The `GITHUB_TOKEN` (automatic in Actions) handles GitHub Releases publishing. If the repo has restricted default token permissions, ensure `contents: write` is allowed (Settings → Actions → General).

## Updating App Icons

The source of truth for the app icon is `apps/desktop/app-icon.svg`. All other icon assets are derived from it. To regenerate after updating the SVG:

1. **Install sharp** in a temp directory (not in the project):
```bash
cd /tmp && mkdir -p icon-gen && cd icon-gen && npm init -y && npm install sharp
```

2. **Run the generation script** from the repo root:
```bash
node apps/desktop/scripts/generate-icons.cjs
```

3. **Convert iconset to icns** (macOS only):
```bash
iconutil -c icns apps/desktop/resources/icon.iconset -o apps/desktop/resources/icon.icns
rm -rf apps/desktop/resources/icon.iconset
```

This produces:
- `resources/icon-1024.png` — full-color 1024x1024 app/dock icon
- `resources/icon.icns` — macOS app bundle icon (used by electron-builder)
- `resources/trayIconTemplate.svg` — must be updated manually to match `app-icon.svg` paths with `fill="#000000"`
- `resources/trayIconTemplate.png` (18x18) and `trayIconTemplate@2x.png` (36x36) — macOS tray template images (black silhouettes)
Follow the format in `.gitmessage`. The subject line must be `<TICKET>: <description>` where TICKET is extracted from the branch name (e.g. `FEAT-68: add no-auth dev mode`). Include bullet-point body, Testing, and Risks sections.
56 changes: 56 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# ClosedLoop Desktop

Electron desktop app for the [ClosedLoop](https://closedloop.ai) platform. Provides a localhost HTTP gateway that bridges the browser-based web app to local development tools -- git operations, file access, code review, and AI-powered coding sessions.

## Prerequisites

- **Node.js** 22+
- **pnpm** 9.15+ (`corepack enable && corepack prepare pnpm@9.15.0 --activate`)
- **just** command runner (`brew install just`)
- **macOS** (Electron desktop builds target macOS only)

## Getting Started

```bash
# Install dependencies
just install

# Build and start the desktop app
just desktop-dev
```

The app binds a local HTTP gateway on port `19432` and shows a tray icon.

## Development

Run `just` to see all available commands. Common ones:

| Command | Description |
|---|---|
| `just desktop-dev` | Build and start Electron |
| `just desktop-no-auth` | Start with gateway auth disabled (dev only) |
| `just desktop-debug-auth` | Start with debug token minting enabled |
| `just desktop-lint` | Run ESLint |
| `just desktop-typecheck` | Run TypeScript type checking |
| `just desktop-test` | Run tests |
| `just desktop-package` | Package as macOS DMG |

## Project Structure

```
apps/
desktop/ Electron main process, localhost HTTP gateway, tray UI
src/
main/ Electron app lifecycle, IPC, cloud relay
server/ HTTP gateway router, operation handlers
shared/ Types and utilities shared between main/server
renderer/ Preload scripts and renderer bridge
resources/ App icons, tray icons
scripts/ Build and icon generation scripts
```

## Releases

Releases are automated via CI. Bump the `version` in `apps/desktop/package.json` as part of your PR. When merged to main, CI builds a universal macOS DMG and publishes it to GitHub Releases. Packaged builds auto-update via `electron-updater`.

See [apps/desktop/CLAUDE.md](apps/desktop/CLAUDE.md) for detailed release and development documentation.
110 changes: 110 additions & 0 deletions apps/desktop/CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
# Desktop App - Development Notes

## Version Bump Rule

**Any commit that touches files in `apps/desktop/` MUST include a version bump in `apps/desktop/package.json`.** Before committing, check whether `package.json` is already modified in the staged changes. If the version was already bumped (e.g. by a prior edit in the same branch), do not bump again. If it was not bumped, increment the patch version (e.g. `0.4.0` -> `0.4.1`) and stage it alongside the other changes. A CI check will fail the PR if desktop files changed without a version bump.

## Testing the Local Gateway (HTTP API)

The desktop Electron app runs a localhost HTTP gateway. To test it manually:

### Authentication

Engineer routes (`/api/engineer/*`) require one of:
1. **Internal gateway token** (`X-Desktop-Gateway-Token`) -- used by cloud command executor for internal calls.
2. **Browser session token** (`X-Desktop-Session-Token`) -- obtained via authenticated challenge-exchange flow. Must be accompanied by an `Origin` header matching the origin bound during exchange.

**Origin-only auth is not supported.** A spoofed `Origin` header alone will not grant access to engineer routes.

### Fail-Closed Behavior (Missing API Key)

The gateway **fails closed** when the desktop API key is not configured:
- **App startup is unaffected** -- the server binds, the UI opens, health endpoint works, cloud relay works.
- **Local-electron browser mode becomes unavailable** -- the challenge-exchange route returns HTTP 503 `"Local gateway auth unavailable: API key required"`. Without a session token, all engineer routes return 401.
- **No silent fallback** -- the browser interceptor surfaces the 503 error to the UI rather than silently degrading or falling back to an insecure auth path.
- **No crashes** -- `getApiKey()` returns `null` safely; no uncaught exceptions.

"Fail closed" means the *feature* is unavailable with an explicit, actionable error -- not that the app crashes.

The health endpoint remains unauthenticated:
```bash
curl -s http://localhost:<PORT>/health
```

The gateway port is visible in the health response or the Electron UI. Typical dev port: `19432`.

### Debug Auth for Development

For manual `curl` testing during development, use the debug auth workflow:

1. Start Electron with debug auth enabled: `just desktop-debug-auth`
2. In the Electron UI Settings panel, click **Mint Debug Token** to generate a short-lived session token.
3. Use the token in curl:

```bash
curl -s \
-H "X-Desktop-Session-Token: <token>" \
-H "Origin: http://localhost" \
"http://localhost:19432/api/engineer/directories?path=/Users/<you>/Source"
```

Debug tokens are short-lived (10 minutes), memory-only, and only available when `CL_LOCAL_GATEWAY_DEBUG_AUTH=1` in an unpackaged build.

### No-Auth Mode for Development

Start with `just desktop-no-auth` to bypass all gateway auth. All engineer routes are open, and the exchange endpoint issues session tokens without challenge verification. Guarded by `!app.isPackaged` -- cannot be enabled in production builds.

### Sandbox Directory Enforcement

Every API route checks target paths against the sandbox base directory via `isPathAllowed()` in `src/server/security.ts`. Paths outside the sandbox return HTTP 403 `{"error": "directory not allowed"}`.

The `sandboxBaseDirectory` is the single source of truth for path access. It is stored in `electron-store` via `SettingsStore` and used to derive a single-entry allowlist at runtime via `buildAllowedDirectories()` in `src/shared/sandbox-policy.ts`.

Hardcoded sensitive paths (`~/.ssh`, `~/.gnupg`, `~/.aws`, `~/Library/Keychains`, `/etc`, `/bin`, `/sbin`) are always denied, even if a parent is allowed.

### Example Test Commands

Obtain a debug token first via `just desktop-debug-auth` + UI "Mint Debug Token" button, then:

```bash
# Should succeed (with valid session token, sandbox directory)
curl -s -H "X-Desktop-Session-Token: <TOKEN>" -H "Origin: http://localhost" \
"http://localhost:19432/api/engineer/directories?path=/Users/<you>/Source"

# Should fail -- no session token (401)
curl -s -H "Origin: http://localhost" "http://localhost:19432/api/engineer/directories?path=/Users/<you>/Source"

# Should fail -- outside sandbox (403)
curl -s -H "X-Desktop-Session-Token: <TOKEN>" -H "Origin: http://localhost" \
"http://localhost:19432/api/engineer/directories?path=/tmp"

# Should fail -- sensitive deny list (403)
curl -s -H "X-Desktop-Session-Token: <TOKEN>" -H "Origin: http://localhost" \
"http://localhost:19432/api/engineer/directories?path=/Users/<you>/.ssh"
```

## Releasing Desktop Builds

Releases are automated via CI. When a PR that touches `apps/desktop/**` is merged to main, the release workflow runs:

1. Reads the `version` from `apps/desktop/package.json`
2. Checks if that version already has a GitHub Release -- if so, skips with a warning
3. Builds a universal macOS DMG via `electron-builder`
4. Publishes the DMG to GitHub Releases and uploads it as a workflow artifact
5. Sends a Slack notification to the team

### Triggering a new release

Bump the `version` field in `apps/desktop/package.json` as part of your PR. When it merges, CI will build and publish automatically.

```jsonc
// apps/desktop/package.json
{ "version": "0.2.0" } // <- bump this
```

If you merge desktop changes **without** bumping the version, the workflow will skip the build and log a warning -- no harm done, no duplicate releases.

### Auto-update for users

- **Packaged builds** (DMG installs) use `electron-updater` to check GitHub Releases every 5 minutes. Users are notified in-app when a new version is available, and it auto-installs on quit.
- **Dev builds** (running from source) compare `origin/main` commit hashes via `git fetch` and offer to pull + rebuild.
35 changes: 35 additions & 0 deletions apps/desktop/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# ClosedLoop Desktop

Electron desktop app providing a localhost HTTP gateway for the ClosedLoop platform.

## Updating App Icons

The source of truth for the app icon is `app-icon.svg`. All other icon assets are derived from it. To regenerate after updating the SVG:

1. **Install sharp** in a temp directory (not in the project):
```bash
cd /tmp && mkdir -p icon-gen && cd icon-gen && npm init -y && npm install sharp
```

2. **Run the generation script** from the repo root:
```bash
node apps/desktop/scripts/generate-icons.cjs
```

3. **Convert iconset to icns** (macOS only):
```bash
iconutil -c icns apps/desktop/resources/icon.iconset -o apps/desktop/resources/icon.icns
rm -rf apps/desktop/resources/icon.iconset
```

This produces:
- `resources/icon-1024.png` -- full-color 1024x1024 app/dock icon
- `resources/icon.icns` -- macOS app bundle icon (used by electron-builder)
- `resources/trayIconTemplate.svg` -- must be updated manually to match `app-icon.svg` paths with `fill="#000000"`
- `resources/trayIconTemplate.png` (18x18) and `trayIconTemplate@2x.png` (36x36) -- macOS tray template images (black silhouettes)

## Required GitHub Secrets

- `SLACK_GITHUB_REPO_WEBHOOK_URL` -- Slack incoming webhook for release notifications

The `GITHUB_TOKEN` (automatic in Actions) handles GitHub Releases publishing. If the repo has restricted default token permissions, ensure `contents: write` is allowed (Settings > Actions > General).
Loading
Loading