Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
7a48ae7
feat(server): add workspace root service with sandbox containment
May 15, 2026
e52cbee
feat(server): configure workspace root on startup
May 15, 2026
fbdce44
feat(server): confine session creation to workspace root
May 15, 2026
bbf26ea
feat(server): always bypass CLI permissions inside workspace root
May 15, 2026
2e506f4
fix(server): use hardenedOptions in recursive startSession retry
May 15, 2026
b22fd52
fix(server): fix workspace root fallback and update stale test assert…
May 15, 2026
ebc9d8a
feat(server): remove computer-use, OAuth, and doctor surfaces from Sa…
May 15, 2026
9be56b2
feat(server): single-user SaaS auth and permissive CORS
May 15, 2026
4564b1f
feat(server): confine filesystem API to workspace root
May 15, 2026
b00be10
feat(server): serve web/dist as the primary static frontend
May 15, 2026
1fb60ed
feat(web): scaffold web frontend that reuses desktop React tree
May 15, 2026
ed14cd2
feat(web): bootstrap the React shell for web runtime
May 15, 2026
4a0d6fd
feat(web): hide Computer Use surfaces in browser runtime
May 15, 2026
1b7a398
docs: add web SaaS quickstart
May 15, 2026
907bac2
test(server): add SaaS startup smoke test
May 15, 2026
ecdecfd
fix(web): resolve all @tauri-apps/* imports through a web stub
May 16, 2026
06e41e7
fix(web): replace desktop React imports with standalone web chat
May 16, 2026
ee09d7e
fix(server): disable managed-OAuth and provider-env stripping in SaaS…
May 16, 2026
e6486b4
chore: add web build artifacts to .gitignore
May 16, 2026
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 .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -74,3 +74,6 @@ runtime/__pycache__

# Codex logs
.codex-logs/
# Web frontend build output
web/dist/
web/bun.lock
6 changes: 5 additions & 1 deletion desktop/src/api/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,11 @@ const ENV_BASE_URL =
? import.meta.env.VITE_DESKTOP_SERVER_URL
: undefined

const DEFAULT_BASE_URL = ENV_BASE_URL || 'http://127.0.0.1:3456'
const DEFAULT_BASE_URL =
ENV_BASE_URL ||
(typeof window !== 'undefined' && window.location?.origin
? window.location.origin
: 'http://127.0.0.1:3456')

let baseUrl = DEFAULT_BASE_URL
let authToken: string | null = null
Expand Down
3 changes: 3 additions & 0 deletions desktop/src/components/chat/ComputerUsePermissionModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { useMemo, useState } from 'react'
import { useTranslation } from '../../i18n'
import { computerUseApi } from '../../api/computerUse'
import { useChatStore } from '../../stores/chatStore'
import { isWebRuntime } from '../../lib/desktopRuntime'
import type {
ComputerUsePermissionRequest,
ComputerUsePermissionResponse,
Expand Down Expand Up @@ -67,6 +68,8 @@ function buildAllowResponse(
}

export function ComputerUsePermissionModal({ sessionId, request }: Props) {
if (isWebRuntime()) return null

const t = useTranslation()
const respondToComputerUsePermission = useChatStore(
(s) => s.respondToComputerUsePermission,
Expand Down
10 changes: 7 additions & 3 deletions desktop/src/components/layout/AppShell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
initializeDesktopServerUrl,
isH5ConnectionRequiredError,
isTauriRuntime,
isWebRuntime,
} from '../../lib/desktopRuntime'
import { TabBar } from './TabBar'
import { StartupErrorView } from './StartupErrorView'
Expand Down Expand Up @@ -39,6 +40,7 @@ export function AppShell() {
const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false)
const t = useTranslation()
const tauriRuntime = isTauriRuntime()
const webRuntime = isWebRuntime()
const isMobileShell = useMobileViewport() && !tauriRuntime
const tabs = useTabStore((s) => s.tabs)
const activeTabId = useTabStore((s) => s.activeTabId)
Expand Down Expand Up @@ -75,7 +77,9 @@ export function AppShell() {
}

try {
await initializeDesktopServerUrl()
if (!webRuntime) {
await initializeDesktopServerUrl()
}
await fetchSettings()

if (!cancelled) {
Expand Down Expand Up @@ -114,7 +118,7 @@ export function AppShell() {

// Listen for macOS native menu navigation events (About / Settings)
useEffect(() => {
if (!tauriRuntime) return
if (!tauriRuntime || webRuntime) return
let unlisten: (() => void) | undefined
import('@tauri-apps/api/event')
.then(({ listen }) =>
Expand Down Expand Up @@ -172,7 +176,7 @@ export function AppShell() {
toggleSidebar()
}

if (!tauriRuntime && h5StartupError) {
if (!tauriRuntime && !webRuntime && h5StartupError) {
return (
<H5ConnectionView
initialServerUrl={h5StartupError.serverUrl}
Expand Down
19 changes: 19 additions & 0 deletions desktop/src/lib/desktopRuntime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -349,3 +349,22 @@ function clearStoredH5Token() {

setAuthToken(null)
}

let webBootstrapApplied = false

export function runWebRuntimeBootstrap(): void {
if (webBootstrapApplied) return
webBootstrapApplied = true

// Always treat the page origin as the API base.
if (typeof window !== 'undefined' && !import.meta.env?.VITE_DESKTOP_SERVER_URL) {
setBaseUrl(window.location.origin)
}

// Single-user: no auth token plumbing.
setAuthToken(null)
}

export function isWebRuntime(): boolean {
return webBootstrapApplied
}
9 changes: 9 additions & 0 deletions desktop/src/pages/ComputerUseSettings.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useState, useEffect, useCallback, useMemo, useRef } from 'react'
import { computerUseApi, type ComputerUseStatus, type SetupResult, type InstalledApp, type AuthorizedApp } from '../api/computerUse'
import { useTranslation } from '../i18n'
import { isWebRuntime } from '../lib/desktopRuntime'

type CheckState = 'loading' | 'ready' | 'error'
const PYTHON_DOWNLOAD_URLS: Record<string, string> = {
Expand Down Expand Up @@ -45,6 +46,14 @@ async function openExternalUrl(url: string) {
}

export function ComputerUseSettings() {
if (isWebRuntime()) {
return (
<div className="p-6 text-sm text-muted-foreground">
Computer Use is disabled in web mode.
</div>
)
}

const t = useTranslation()
const [status, setStatus] = useState<ComputerUseStatus | null>(null)
const [checkState, setCheckState] = useState<CheckState>('loading')
Expand Down
38 changes: 38 additions & 0 deletions docs/web-saas/01-quickstart.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Web SaaS Quickstart

## Run the server

```bash
bun install
CC_HAHA_WORKSPACES_ROOT="$(pwd)/workspaces" SERVER_PORT=3456 bun run src/server/index.ts
```

The server creates `workspaces/` if it does not exist. Every session lives in
`workspaces/<workspaceName>/`. If `workspaceName` is omitted in
`POST /api/sessions`, the server uses the `sessionId` itself as the directory
name. Multiple sessions may share the same `workspaceName`.

## Build and serve the web frontend

```bash
cd web
bun install
bun run build
```

The build output lands in `web/dist/`, which is served by the existing Bun
process (see `src/server/staticH5.ts`).

For a hot-reload dev loop run `bun run dev` instead and connect the
browser to `http://127.0.0.1:5173` — the Vite dev server proxies `/api`
and `/ws` to the Bun server on port 3456.

## What is intentionally disabled in this profile

- Computer Use
- H5 access tokens / OAuth callbacks
- Tauri-specific UI (window controls, native notifications, updater)
- Doctor self-repair flows (the diagnostics endpoints stay available)
- All tool/file/agent permission prompts: actions inside the workspace
root are pre-authorised; anything that would touch a path outside the
workspace root is denied at the API boundary.
4 changes: 2 additions & 2 deletions src/constants/prompts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ function getSimpleIntroSection(
): string {
// eslint-disable-next-line custom-rules/prompt-spacing
return `
You are an interactive agent that helps users ${outputStyleConfig !== null ? 'according to your "Output Style" below, which describes how you should respond to user queries.' : 'with software engineering tasks.'} Use the instructions below and the tools available to you to assist the user.
You are an interactive agent that helps users ${outputStyleConfig !== null ? 'according to your "Output Style" below, which describes how you should respond to user queries.' : 'with a wide range of tasks, including general questions, analysis, writing, and software engineering work.'} Use the instructions below and the tools available to you to assist the user.

${CYBER_RISK_INSTRUCTION}
IMPORTANT: You must NEVER generate or guess URLs for the user unless you are confident that the URLs are for helping the user with programming. You may use URLs provided by the user in their messages or local files.`
Expand Down Expand Up @@ -219,7 +219,7 @@ function getSimpleDoingTasksSection(): string {
]

const items = [
`The user will primarily request you to perform software engineering tasks. These may include solving bugs, adding new functionality, refactoring code, explaining code, and more. When given an unclear or generic instruction, consider it in the context of these software engineering tasks and the current working directory. For example, if the user asks you to change "methodName" to snake case, do not reply with just "method_name", instead find the method in the code and modify the code.`,
`The user may ask you to perform many kinds of tasks, including answering general questions, analyzing information, writing content, and solving software engineering problems. When a request involves the current working directory or codebase, treat it as a software engineering task and inspect the relevant files instead of answering abstractly. For example, if the user asks you to change "methodName" to snake case, do not reply with just "method_name", instead find the method in the code and modify the code.`,
`You are highly capable and often allow users to complete ambitious tasks that would otherwise be too complex or take too long. You should defer to user judgement about whether a task is too large to attempt.`,
// @[MODEL LAUNCH]: capy v8 assertiveness counterweight (PR #24302) — un-gate once validated on external via A/B
...(process.env.USER_TYPE === 'ant'
Expand Down
9 changes: 9 additions & 0 deletions src/server/__tests__/auth-saas.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { describe, expect, it } from 'bun:test'
import { requireAuth } from '../middleware/auth.js'

describe('requireAuth (SaaS single-user)', () => {
it('returns null for any request', async () => {
const req = new Request('http://localhost/api/sessions', { method: 'GET' })
expect(await requireAuth(req)).toBeNull()
})
})
10 changes: 10 additions & 0 deletions src/server/__tests__/conversation-permission-mode.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { describe, expect, it } from 'bun:test'
import { ConversationService } from '../services/conversationService.js'

describe('ConversationService permission profile', () => {
it('always uses --dangerously-skip-permissions in SaaS mode', () => {
const svc = new ConversationService()
const args = (svc as any).getPermissionArgs('default', false) as string[]
expect(args).toEqual(['--dangerously-skip-permissions'])
})
})
Loading
Loading