diff --git a/src/servers/api/auth/bootstrap.ts b/src/servers/api/auth/bootstrap.ts index 225c3a05..934eab1b 100644 --- a/src/servers/api/auth/bootstrap.ts +++ b/src/servers/api/auth/bootstrap.ts @@ -4,6 +4,7 @@ import { officerdb, count, Users } from 'officerdb'; import { sign, verify } from '@@/jwt'; import argon2 from 'argon2'; import * as errors from '@@/custom-errors'; +import { syncUserPiConfig } from '@@/api/server-settings/sync-user-pi-config'; import { validatePassword } from './validate-password'; export const bootstrapHandler: Handler = async function (ctx) { @@ -59,5 +60,6 @@ export const bootstrapHandler: Handler = async function (ctx) { }).returning(); if (!insertedUsers || insertedUsers.length === 0) throw errors.INTERNAL_SERVER_ERROR('Failed to create user'); + syncUserPiConfig(payload.email).catch(() => {}); return ctx.json({ ok: true }); }; diff --git a/src/servers/api/auth/signin.ts b/src/servers/api/auth/signin.ts index 5243a700..1e5f77fb 100755 --- a/src/servers/api/auth/signin.ts +++ b/src/servers/api/auth/signin.ts @@ -4,6 +4,7 @@ import { join } from 'node:path'; import { officerdb, eq, and, Users, Passkeys } from 'officerdb'; import { sign } from '@@/jwt'; import { getClaudeDir } from '@@/data-path'; +import { syncUserPiConfig } from '@@/api/server-settings/sync-user-pi-config'; import argon2 from 'argon2'; import * as errors from '@@/custom-errors'; @@ -29,6 +30,7 @@ export const signinHandler: Handler = async function (ctx) { const { id, name, role } = dbUser; mkdir(join(getClaudeDir(email), 'archived'), { recursive: true }).catch(() => {}); + syncUserPiConfig(email).catch(() => {}); const tokenUser = { id, email, name, role, passkeys: passkeys.length }; diff --git a/src/servers/api/pi/pi-bridge.ts b/src/servers/api/pi/pi-bridge.ts index c6e26d29..cc489ba0 100644 --- a/src/servers/api/pi/pi-bridge.ts +++ b/src/servers/api/pi/pi-bridge.ts @@ -6,26 +6,67 @@ import { logger } from "./logger"; export type PiEventHandler = (event: PiEvent) => void; +type SandboxOptions = { + userId: number; +}; + +const CONTAINER_HOME = '/home/officer'; +const CONTAINER_PI_CONFIG = '/home/officer/.pi/agent'; + export async function spawnPi( cwd: string, model: string, - onEvent: PiEventHandler + onEvent: PiEventHandler, + sandbox?: SandboxOptions, ): Promise { - const storedKeys = await readApiKeys(); - - const args = ['pi', '--mode', 'rpc', '--no-extensions', '--no-skills', '--no-prompt-templates', '--no-themes']; - if (model) args.push('--model', model); + let proc: Subprocess; - const proc = Bun.spawn(args, { - cwd, - stdin: 'pipe', - stdout: 'pipe', - stderr: 'pipe', - env: { ...process.env, ...storedKeys, PI_CODING_AGENT_DIR: PI_CONFIG_DIR }, - }); + if (sandbox) { + const storedKeys = await readApiKeys(); + const dockerPath = Bun.which('docker') ?? 'docker'; + const containerId = `officer-terminal-${sandbox.userId}`; + const piArgs = ['pi', '--mode', 'rpc', '--no-extensions', '--no-skills', '--no-prompt-templates', '--no-themes']; + if (model) piArgs.push('--model', model); + + // Build env flags: Pi config dir + all stored API keys + const envFlags = [ + '-e', `PI_CODING_AGENT_DIR=${CONTAINER_PI_CONFIG}`, + '-e', `HOME=${CONTAINER_HOME}`, + ]; + for (const [key, value] of Object.entries(storedKeys)) { + if (value?.trim()) envFlags.push('-e', `${key}=${value.trim()}`); + } + + proc = Bun.spawn([ + dockerPath, 'exec', '-i', + '-w', CONTAINER_HOME, + ...envFlags, + containerId, + ...piArgs, + ], { + stdin: 'pipe', + stdout: 'pipe', + stderr: 'pipe', + }); + + logger.info('Spawned Pi in container', { containerId, model }); + } else { + const storedKeys = await readApiKeys(); + const args = ['pi', '--mode', 'rpc', '--no-extensions', '--no-skills', '--no-prompt-templates', '--no-themes']; + if (model) args.push('--model', model); + + proc = Bun.spawn(args, { + cwd, + stdin: 'pipe', + stdout: 'pipe', + stderr: 'pipe', + env: { ...process.env, ...storedKeys, PI_CODING_AGENT_DIR: PI_CONFIG_DIR }, + }); + } // Read stdout JSON event stream (runs in background) - const reader = proc.stdout.getReader(); + const stdout = proc.stdout as ReadableStream; + const reader = stdout.getReader(); const decoder = new TextDecoder(); let buffer = ''; let streamBuffer = ''; @@ -63,7 +104,8 @@ export async function spawnPi( })(); // Stderr → debug log - const stderrReader = proc.stderr.getReader(); + const stderr = proc.stderr as ReadableStream; + const stderrReader = stderr.getReader(); const stderrDecoder = new TextDecoder(); (async () => { try { diff --git a/src/servers/api/pi/types.ts b/src/servers/api/pi/types.ts index 05a41c40..3e50cea6 100644 --- a/src/servers/api/pi/types.ts +++ b/src/servers/api/pi/types.ts @@ -46,6 +46,7 @@ export type ClientMessage = sessionId?: string; model?: string; cwd?: string; + sandboxed?: boolean; groupSlug?: string; attachmentIds?: string[]; } @@ -130,8 +131,10 @@ export type PiEvent = export type UserSession = { sessionId: string; email: string; + userId?: number; cwd: string; model: string; + sandboxed?: boolean; piProcess: any | null; ws: any | null; lastActivity: number; diff --git a/src/servers/api/pi/websocket.ts b/src/servers/api/pi/websocket.ts index 578e8c33..a0c32f02 100644 --- a/src/servers/api/pi/websocket.ts +++ b/src/servers/api/pi/websocket.ts @@ -220,9 +220,9 @@ function createEventHandler(sessionId: string, model: string, cwd: string) { async function handleChat( ws: ServerWebSocket, - msg: { prompt: string; sessionId?: string; model?: string; cwd?: string; groupSlug?: string; attachmentIds?: string[] } + msg: { prompt: string; sessionId?: string; model?: string; cwd?: string; sandboxed?: boolean; groupSlug?: string; attachmentIds?: string[] } ): Promise { - const { email } = ws.data; + const { email, userId } = ws.data; const sessionId = msg.sessionId || randomUUID(); // Use provided model, or fall back to user default, or use system default @@ -251,18 +251,19 @@ async function handleChat( const cwd = msg.cwd || getHomeDir(email); const groupSlug = msg.groupSlug || null; + const sandboxed = msg.sandboxed ?? false; const session = sessionManager.getOrCreate(sessionId, email, cwd, model, groupSlug); + session.sandboxed = sandboxed; + session.userId = userId; sessionManager.attachWs(sessionId, ws); wsToSessionMap.set(ws as any, sessionId); sendToClient(ws, { type: 'session:init', sessionId, model, cwd }); - - // Spawn Pi process if not already running if (!session.piProcess) { try { const onEvent = createEventHandler(sessionId, model, cwd); - session.piProcess = await piBridge.spawnPi(cwd, model, onEvent); - logger.info('Spawned Pi process for session', { sessionId, model, cwd }); + session.piProcess = await piBridge.spawnPi(cwd, model, onEvent, sandboxed ? { userId } : undefined); + logger.info('Spawned Pi process for session', { sessionId, model, cwd, sandboxed }); } catch (err) { logger.error('Failed to spawn Pi process', { sessionId, model, error: String(err) }); sendToClient(ws, { type: 'error', message: 'Failed to start Pi process' }); @@ -327,9 +328,10 @@ async function handleResume( // Spawn fresh Pi process if needed if (!session.piProcess) { try { + const sandbox = session.sandboxed && session.userId ? { userId: session.userId } : undefined; const onEvent = createEventHandler(sessionId, session.model, session.cwd); - session.piProcess = await piBridge.spawnPi(session.cwd, session.model, onEvent); - logger.info('Spawned fresh Pi process for resumed session', { sessionId, model: session.model }); + session.piProcess = await piBridge.spawnPi(session.cwd, session.model, onEvent, sandbox); + logger.info('Spawned fresh Pi process for resumed session', { sessionId, model: session.model, sandboxed: session.sandboxed }); } catch (err) { logger.error('Failed to spawn Pi process for resume', { sessionId, error: String(err) }); sendToClient(ws, { type: 'error', message: 'Failed to start Pi process' }); diff --git a/src/servers/api/server-settings/pi-mono.ts b/src/servers/api/server-settings/pi-mono.ts index 349a3693..4ed0b86f 100644 --- a/src/servers/api/server-settings/pi-mono.ts +++ b/src/servers/api/server-settings/pi-mono.ts @@ -2,12 +2,14 @@ import { join } from 'node:path'; import { createRouter } from '../../create-router'; import { DATA_PATH } from '../../data-path'; import { syncLocalProvidersToPiConfig } from './sync-pi-config'; +import { syncAllUserPiConfigs } from './sync-user-pi-config'; import { invalidateModelCache } from '../pi/list-models'; export const piMonoRouter = createRouter(); const API_KEYS_FILE = join(DATA_PATH, 'pi_mono_api_keys.json'); const LOCAL_PROVIDERS_FILE = join(DATA_PATH, 'pi_mono_local_providers.json'); +const ACCESS_POLICY_FILE = join(DATA_PATH, 'pi_access_policy.json'); // --- Local provider types --- @@ -163,7 +165,25 @@ async function writeApiKeys(keys: Record) { await Bun.write(API_KEYS_FILE, JSON.stringify(keys, null, 2)); } -const PROVIDERS: { key: string; env: string[] }[] = [ +export type AccessPolicy = { + allowedModels: string[]; // e.g. ["anthropic:anthropic/claude-sonnet-4"] +}; + +export async function readAccessPolicy(): Promise { + try { + const file = Bun.file(ACCESS_POLICY_FILE); + if (!(await file.exists())) return { allowedModels: [] }; + return (await file.json()) as AccessPolicy; + } catch { + return { allowedModels: [] }; + } +} + +async function writeAccessPolicy(policy: AccessPolicy) { + await Bun.write(ACCESS_POLICY_FILE, JSON.stringify(policy, null, 2)); +} + +export const PROVIDERS: { key: string; env: string[] }[] = [ { key: 'OpenAI', env: ['OPENAI_API_KEY'] }, { key: 'Google', env: ['GOOGLE_API_KEY', 'GEMINI_API_KEY'] }, { key: 'OpenCode Zen', env: ['OPENCODE_API_KEY'] }, @@ -216,6 +236,7 @@ piMonoRouter.put('/api-keys', async (ctx) => { } await writeApiKeys(keys); invalidateModelCache(); + syncAllUserPiConfigs().catch(() => {}); return ctx.json({ ok: true }); }); @@ -304,7 +325,8 @@ piMonoRouter.post('/local-providers', async (ctx) => { // Sync to Pi config so Pi knows about this provider await syncLocalProvidersToPiConfig(); - + syncAllUserPiConfigs().catch(() => {}); + return ctx.json({ ...provider, auth: provider.auth ? { type: provider.auth.type } : undefined }); }); @@ -317,7 +339,25 @@ piMonoRouter.delete('/local-providers/:id', async (ctx) => { // Sync to Pi config to remove this provider await syncLocalProvidersToPiConfig(); - + syncAllUserPiConfigs().catch(() => {}); + + return ctx.json({ ok: true }); +}); + +// --- Access policy --- + +piMonoRouter.get('/access-policy', async (ctx) => { + const policy = await readAccessPolicy(); + return ctx.json(policy); +}); + +piMonoRouter.put('/access-policy', async (ctx) => { + const body = await ctx.req.json(); + const policy: AccessPolicy = { + allowedModels: Array.isArray(body.allowedModels) ? body.allowedModels : [], + }; + await writeAccessPolicy(policy); + await syncAllUserPiConfigs(); return ctx.json({ ok: true }); }); diff --git a/src/servers/api/server-settings/sync-user-pi-config.ts b/src/servers/api/server-settings/sync-user-pi-config.ts new file mode 100644 index 00000000..8f840155 --- /dev/null +++ b/src/servers/api/server-settings/sync-user-pi-config.ts @@ -0,0 +1,172 @@ +import { join } from 'node:path'; +import { mkdir, copyFile } from 'node:fs/promises'; +import { PI_CONFIG_DIR, getUserPiConfigDir } from '../../data-path'; +import { readApiKeys, readAccessPolicy, PROVIDERS } from './pi-mono'; +import { officerdb, Users } from 'officerdb'; + +const PI_MODELS_FILE = join(PI_CONFIG_DIR, 'models.json'); +const PI_SETTINGS_FILE = join(PI_CONFIG_DIR, 'settings.json'); + +type PiModelConfig = { + providers: Record; +}; + +type BuildFilteredConfigParams = { + appConfig: PiModelConfig; + allowedModels: string[]; + apiKeys: Record; +}; + +/** + * Maps lowercase provider names to their env var names. + * e.g. "openai" → ["OPENAI_API_KEY"], "google" → ["GOOGLE_API_KEY", "GEMINI_API_KEY"] + */ +function buildProviderEnvMap(): Record { + const map: Record = {}; + for (const p of PROVIDERS) { + map[p.key.toLowerCase()] = p.env; + } + return map; +} + +/** + * Build a filtered models.json config with API keys embedded. + * - If allowedModels is empty, all models pass through (same convention as enabledModels). + * - Parse policy key: "anthropic:anthropic/claude-sonnet-4" → provider "anthropic", model id "claude-sonnet-4" + * - Local providers (officer-local-*) pass through unchanged with keys already embedded. + */ +function buildFilteredConfig({ appConfig, allowedModels, apiKeys }: BuildFilteredConfigParams): PiModelConfig { + const envMap = buildProviderEnvMap(); + const hasPolicy = allowedModels.length > 0; + + // Pre-parse policy into a map: provider → Set + const policyMap = new Map>(); + if (hasPolicy) { + for (const key of allowedModels) { + const colonIdx = key.indexOf(':'); + if (colonIdx === -1) continue; + const provider = key.slice(0, colonIdx); + const fullModelId = key.slice(colonIdx + 1); + // Strip "provider/" prefix to get the models.json model id + const slashIdx = fullModelId.indexOf('/'); + const modelId = slashIdx !== -1 ? fullModelId.slice(slashIdx + 1) : fullModelId; + + if (!policyMap.has(provider)) policyMap.set(provider, new Set()); + policyMap.get(provider)!.add(modelId); + } + } + + const result: PiModelConfig = { providers: {} }; + + for (const [providerId, providerConfig] of Object.entries(appConfig.providers)) { + // Local providers pass through unchanged + if (providerId.startsWith('officer-local-')) { + result.providers[providerId] = providerConfig; + continue; + } + + const providerLower = providerId.toLowerCase(); + + // Filter models if policy is set + let models = providerConfig.models; + if (hasPolicy) { + const allowedSet = policyMap.get(providerLower); + if (!allowedSet) continue; // provider not in policy at all + models = models.filter((m) => allowedSet.has(m.id)); + if (models.length === 0) continue; + } + + // Embed API key + let apiKey = providerConfig.apiKey; + const envVars = envMap[providerLower]; + if (envVars) { + for (const envVar of envVars) { + const value = apiKeys[envVar]; + if (value?.trim()) { + apiKey = value.trim(); + break; + } + } + } + + result.providers[providerId] = { ...providerConfig, models, apiKey }; + } + + return result; +} + +async function readAppModelsConfig(): Promise { + try { + const file = Bun.file(PI_MODELS_FILE); + if (!(await file.exists())) return { providers: {} }; + return (await file.json()) as PiModelConfig; + } catch { + return { providers: {} }; + } +} + +export async function syncUserPiConfig(email: string): Promise { + const [appConfig, policy, apiKeys] = await Promise.all([ + readAppModelsConfig(), + readAccessPolicy(), + readApiKeys(), + ]); + + const filtered = buildFilteredConfig({ + appConfig, + allowedModels: policy.allowedModels, + apiKeys, + }); + + const userDir = getUserPiConfigDir(email); + await mkdir(userDir, { recursive: true }); + await Bun.write(join(userDir, 'models.json'), JSON.stringify(filtered, null, 2)); + + // Copy settings.json from app-level Pi config + await copyFile(PI_SETTINGS_FILE, join(userDir, 'settings.json')).catch(() => {}); +} + +export async function syncAllUserPiConfigs(): Promise { + const users = await officerdb.select({ email: Users.email }).from(Users); + if (users.length === 0) return; + + const [appConfig, policy, apiKeys] = await Promise.all([ + readAppModelsConfig(), + readAccessPolicy(), + readApiKeys(), + ]); + + const filtered = buildFilteredConfig({ + appConfig, + allowedModels: policy.allowedModels, + apiKeys, + }); + + const configJson = JSON.stringify(filtered, null, 2); + + for (const user of users) { + try { + const userDir = getUserPiConfigDir(user.email); + await mkdir(userDir, { recursive: true }); + await Bun.write(join(userDir, 'models.json'), configJson); + await copyFile(PI_SETTINGS_FILE, join(userDir, 'settings.json')).catch(() => {}); + } catch (err) { + console.error(`[sync] Failed to sync Pi config for ${user.email}:`, err); + } + } + + console.log(`[bootstrap] Synced Pi config for ${users.length} user(s)`); +} diff --git a/src/servers/api/terminal/Dockerfile.terminal-sidecar b/src/servers/api/terminal/Dockerfile.terminal-sidecar index 9685a31c..13a81e73 100644 --- a/src/servers/api/terminal/Dockerfile.terminal-sidecar +++ b/src/servers/api/terminal/Dockerfile.terminal-sidecar @@ -5,7 +5,10 @@ RUN apt-get update \ python3 make gcc g++ zsh git curl wget ca-certificates \ sudo gosu locales \ zip unzip tree btop net-tools tmux \ + procps psmisc lsof less file man-db \ + ripgrep fd-find jq htop \ && sed -i '/en_US.UTF-8/s/^# //g' /etc/locale.gen && locale-gen \ + && ln -sf /usr/bin/fdfind /usr/local/bin/fd \ && apt-get clean ENV LANG=en_US.UTF-8 LC_ALL=en_US.UTF-8 @@ -47,6 +50,8 @@ RUN curl -fsSL "https://github.com/jesseduffield/lazygit/releases/download/v${LA && rm -rf /tmp/lazygit.tar.gz /tmp/LICENSE /tmp/README.md +RUN npm install -g @mariozechner/pi-coding-agent + RUN mkdir -p /home/officer/Documents /home/officer/Downloads /home/officer/Music /home/officer/Videos /home/officer/Pictures /home/officer/Desktop /home/officer/Projects WORKDIR /home/officer diff --git a/src/servers/api/terminal/websocket.ts b/src/servers/api/terminal/websocket.ts index 8bcb1577..2125294e 100644 --- a/src/servers/api/terminal/websocket.ts +++ b/src/servers/api/terminal/websocket.ts @@ -3,6 +3,7 @@ import { mkdirSync, statSync } from 'node:fs'; import { dirname, join } from 'node:path'; import { fileURLToPath } from 'node:url'; import { getHomeDir } from '@@/data-path'; +import { syncUserPiConfig } from '@@/api/server-settings/sync-user-pi-config'; import { officerdb, Users } from 'officerdb'; type WSData = { userId: number; email: string; role: string; sandboxed: boolean; sessionId?: string; cwd?: string; cols?: number; rows?: number }; @@ -274,6 +275,9 @@ export const initTerminalSidecars = async () => { mkdirSync(dirname(homeDir), { recursive: true }); mkdirSync(homeDir, { recursive: true }); try { + await syncUserPiConfig(user.email).catch((err) => { + console.error(`[terminal] failed to sync Pi config for ${user.email}:`, err); + }); await ensureDockerContainer(user.email, user.id, homeDir); console.log(`[terminal] sidecar ready for ${user.email}`); } catch (err) { diff --git a/src/servers/bootstrap.ts b/src/servers/bootstrap.ts index 52deda7e..6207a647 100644 --- a/src/servers/bootstrap.ts +++ b/src/servers/bootstrap.ts @@ -3,6 +3,7 @@ import { join } from 'node:path'; import { homedir } from 'node:os'; import { DATA_PATH, PI_CONFIG_DIR } from './data-path'; import { syncLocalProvidersToPiConfig } from './api/server-settings/sync-pi-config'; +import { syncAllUserPiConfigs } from './api/server-settings/sync-user-pi-config'; mkdirSync(DATA_PATH, { recursive: true }); mkdirSync(PI_CONFIG_DIR, { recursive: true }); @@ -69,4 +70,8 @@ function seedPiConfig(): void { await syncLocalProvidersToPiConfig().catch(err => { console.error('[bootstrap] Failed to sync local providers to Pi config:', err); }); + + await syncAllUserPiConfigs().catch(err => { + console.error('[bootstrap] Failed to sync user Pi configs:', err); + }); })(); diff --git a/src/servers/data-path.ts b/src/servers/data-path.ts index 1533cab4..b8d6cbc5 100644 --- a/src/servers/data-path.ts +++ b/src/servers/data-path.ts @@ -28,6 +28,8 @@ export const getArchivedSessionDir = (email: string, sessionId: string) => export const getHomeDir = (email: string) => join(DATA_PATH, email, 'home'); +export const getUserPiConfigDir = (email: string) => join(DATA_PATH, email, 'home', '.pi', 'agent'); + export const getUserSettingsDir = (email: string) => join(DATA_PATH, email, 'settings'); export const getUserSettingsFile = (email: string) => join(DATA_PATH, email, 'settings', 'settings.json'); diff --git a/src/workspaces/officerdev/src/apps/Chat/EmbeddableChat/EmbeddableChat.tsx b/src/workspaces/officerdev/src/apps/Chat/EmbeddableChat/EmbeddableChat.tsx index 3f00a4ca..5868a49b 100644 --- a/src/workspaces/officerdev/src/apps/Chat/EmbeddableChat/EmbeddableChat.tsx +++ b/src/workspaces/officerdev/src/apps/Chat/EmbeddableChat/EmbeddableChat.tsx @@ -16,6 +16,7 @@ type EmbeddableChatProps = { promptPrefix?: string; className?: string; cwd?: { root?: string; path: string }; + sandboxed?: boolean; autoSend?: boolean; chat?: UsePiChatType; }; diff --git a/src/workspaces/officerdev/src/apps/Chat/EmbeddableChat/useEmbeddableChat.ts b/src/workspaces/officerdev/src/apps/Chat/EmbeddableChat/useEmbeddableChat.ts index 080418d7..42cce2d7 100644 --- a/src/workspaces/officerdev/src/apps/Chat/EmbeddableChat/useEmbeddableChat.ts +++ b/src/workspaces/officerdev/src/apps/Chat/EmbeddableChat/useEmbeddableChat.ts @@ -17,12 +17,13 @@ type UseEmbeddableChatParams = { defaultInput?: string; promptPrefix?: string; cwd?: { root?: string; path: string }; + sandboxed?: boolean; autoSend?: boolean; chat?: UsePiChatType; }; export function useEmbeddableChat(params: UseEmbeddableChatParams) { - const { initialMessage, defaultInput = '', promptPrefix, cwd, autoSend = false, chat: externalChat } = params; + const { initialMessage, defaultInput = '', promptPrefix, cwd, sandboxed, autoSend = false, chat: externalChat } = params; const internalChat = usePiChat(params.sessionId, params.initialModel); const chat = externalChat ?? internalChat; @@ -79,6 +80,8 @@ export function useEmbeddableChat(params: UseEmbeddableChatParams) { !sessionId && ids.length > 0 ? ids : undefined, images.length > 0 ? images : undefined, cwdForFirst, + undefined, + sandboxed, ); attachmentManager.clearAttachments(); @@ -150,6 +153,8 @@ export function useEmbeddableChat(params: UseEmbeddableChatParams) { initialMessage.attachmentIds, initialMessage.images, initialMessage.cwd, + undefined, + sandboxed, ); } }, [initialMessage, isConnected]); diff --git a/src/workspaces/officerdev/src/apps/ChatHistory/ChatDetailPanel.tsx b/src/workspaces/officerdev/src/apps/ChatHistory/ChatDetailPanel.tsx index bca4f25e..66413437 100644 --- a/src/workspaces/officerdev/src/apps/ChatHistory/ChatDetailPanel.tsx +++ b/src/workspaces/officerdev/src/apps/ChatHistory/ChatDetailPanel.tsx @@ -133,6 +133,7 @@ function NewChat() { const chat = usePiChat(undefined, locationState?.model); + const sandboxed = cwdMode === 'user'; const cwd = cwdMode === 'host' ? { path: getHostHome() } : locationState?.cwd; const initialMessage = locationState?.initialMessage @@ -160,6 +161,7 @@ function NewChat() { initialMessage={initialMessage} defaultInput={locationState?.prefillInput ?? ''} cwd={cwd} + sandboxed={sandboxed} className="flex-1 min-h-0" /> diff --git a/src/workspaces/officerdev/src/hooks/usePiChat.ts b/src/workspaces/officerdev/src/hooks/usePiChat.ts index d2915e8f..71330063 100644 --- a/src/workspaces/officerdev/src/hooks/usePiChat.ts +++ b/src/workspaces/officerdev/src/hooks/usePiChat.ts @@ -257,6 +257,7 @@ export function usePiChat(initialSessionId?: string, initialModel?: string | nul images?: { filename: string; dataUrl: string }[], cwdParam?: { root?: string; path: string }, groupSlug?: string | null, + sandboxed?: boolean, ) { // Mark session as started on first message if (!hasStarted) { @@ -282,6 +283,7 @@ export function usePiChat(initialSessionId?: string, initialModel?: string | nul sessionId: sessionIdRef.current, ...(selectedModel ? { model: selectedModel } : {}), ...(cwdParam?.path ? { cwd: cwdParam.path } : {}), + ...(sandboxed !== undefined ? { sandboxed } : {}), ...(groupSlug !== undefined ? { groupSlug } : {}), ...(attachmentIds?.length ? { attachmentIds } : {}), ...(imageData?.length ? { images: imageData } : {}),