fix pi cwd resolution and super admin home dir for all pi endpoints
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -6,7 +6,7 @@ import { readSearxngConfig } from '../server-settings/searxng';
|
||||
import {
|
||||
PI_CONFIG_DIR,
|
||||
DATA_PATH,
|
||||
getHomeDir,
|
||||
getHomeDirForRole,
|
||||
getGlobalSkillsDir,
|
||||
getUserSkillsDir,
|
||||
getGlobalExtensionsDir,
|
||||
@@ -237,6 +237,7 @@ async function resolveApiKeyForModel(model: string): Promise<string | null> {
|
||||
type SpawnPiOptions = {
|
||||
sessionFile?: string;
|
||||
username?: string;
|
||||
role?: string;
|
||||
};
|
||||
|
||||
export async function spawnPi(
|
||||
@@ -275,7 +276,7 @@ export async function spawnPi(
|
||||
mkdirSync(cwd, { recursive: true });
|
||||
}
|
||||
|
||||
const homeDir = getHomeDir(email);
|
||||
const homeDir = getHomeDirForRole(email, options?.role ?? null);
|
||||
const toolsDirs = [getGlobalToolsDir(), getUserToolsDir(email)].join(':');
|
||||
const browserRelayEnv = await getBrowserRelayEnv(userId);
|
||||
const apifyToken = await getApifyToken();
|
||||
@@ -550,13 +551,14 @@ export function killPi(process: Subprocess): void {
|
||||
}
|
||||
|
||||
/** Build env vars needed by Officer tools when running on the host. */
|
||||
export async function buildHostToolEnv(userId: number, email: string): Promise<Record<string, string>> {
|
||||
export async function buildHostToolEnv(userId: number, email: string, role?: string): Promise<Record<string, string>> {
|
||||
const browserRelayEnv = await getBrowserRelayEnv(userId);
|
||||
const apifyToken = await getApifyToken();
|
||||
const homeDir = getHomeDirForRole(email, role ?? null);
|
||||
|
||||
return {
|
||||
HOME: getHomeDir(email),
|
||||
OFFICER_USER_HOME: getHomeDir(email),
|
||||
HOME: homeDir,
|
||||
OFFICER_USER_HOME: homeDir,
|
||||
OFFICER_USER_ROOT: join(DATA_PATH, email),
|
||||
OFFICER_RESOURCES: buildResourcesEnv(),
|
||||
OFFICER_EMAIL_DB: join(DATA_PATH, email, 'emails.db'),
|
||||
|
||||
+13
-13
@@ -4,7 +4,7 @@ import * as storage from './storage';
|
||||
import { readLocalProviders } from '../server-settings/pi-mono';
|
||||
import { readSttConfig } from '../server-settings/stt';
|
||||
import { listPiModels } from './list-models';
|
||||
import { getHomeDir } from '../../data-path';
|
||||
import { getHomeDirForRole } from '../../data-path';
|
||||
import { resolveBaseCwd } from './websocket';
|
||||
import { logger } from './logger';
|
||||
|
||||
@@ -50,8 +50,8 @@ piRestRouter.post('/pi/sessions', async (ctx: Context) => {
|
||||
}
|
||||
|
||||
const body = await ctx.req.json().catch(() => ({}));
|
||||
const userHome = getHomeDir(user.email);
|
||||
const filterCwd = body.cwd ? resolveBaseCwd(user.email, body.cwdRoot, body.cwd) : null;
|
||||
const userHome = getHomeDirForRole(user.email, user.role);
|
||||
const filterCwd = body.cwd ? resolveBaseCwd(user.email, user.role, body.cwd) : null;
|
||||
const contextFilter = body.context
|
||||
? { context: body.context as string, contextId: body.contextId as string | undefined }
|
||||
: undefined;
|
||||
@@ -83,7 +83,7 @@ piRestRouter.get('/pi/sessions/:sessionId', async (ctx: Context) => {
|
||||
return ctx.json({ error: 'Session ID required' }, 400);
|
||||
}
|
||||
|
||||
const userHome = getHomeDir(user.email);
|
||||
const userHome = getHomeDirForRole(user.email, user.role);
|
||||
|
||||
try {
|
||||
// Try loading from root first
|
||||
@@ -150,7 +150,7 @@ piRestRouter.patch('/pi/sessions/:sessionId', async (ctx: Context) => {
|
||||
return ctx.json({ error: 'Title is required and must be a string' }, 400);
|
||||
}
|
||||
|
||||
const userHome = getHomeDir(user.email);
|
||||
const userHome = getHomeDirForRole(user.email, user.role);
|
||||
|
||||
try {
|
||||
// Find the session (root or in group)
|
||||
@@ -207,7 +207,7 @@ piRestRouter.delete('/pi/sessions/:sessionId', async (ctx: Context) => {
|
||||
return ctx.json({ error: 'Session ID required' }, 400);
|
||||
}
|
||||
|
||||
const userHome = getHomeDir(user.email);
|
||||
const userHome = getHomeDirForRole(user.email, user.role);
|
||||
|
||||
try {
|
||||
// Find the session (root or in group)
|
||||
@@ -266,7 +266,7 @@ piRestRouter.delete('/pi/sessions', async (ctx: Context) => {
|
||||
const contextFilter = body.context
|
||||
? { context: body.context as string, contextId: body.contextId as string | undefined }
|
||||
: undefined;
|
||||
const userHome = getHomeDir(user.email);
|
||||
const userHome = getHomeDirForRole(user.email, user.role);
|
||||
|
||||
try {
|
||||
const allSessions = await storage.listUserSessions(userHome, contextFilter);
|
||||
@@ -311,7 +311,7 @@ piRestRouter.get('/pi/sessions/search', async (ctx: Context) => {
|
||||
return ctx.json({ error: 'Query parameter required' }, 400);
|
||||
}
|
||||
|
||||
const userHome = getHomeDir(user.email);
|
||||
const userHome = getHomeDirForRole(user.email, user.role);
|
||||
|
||||
try {
|
||||
const results = await storage.searchSessions(userHome, query);
|
||||
@@ -342,7 +342,7 @@ piRestRouter.post('/pi/groups', async (ctx: Context) => {
|
||||
return ctx.json({ error: 'Slug is required and must be a string' }, 400);
|
||||
}
|
||||
|
||||
const userHome = getHomeDir(user.email);
|
||||
const userHome = getHomeDirForRole(user.email, user.role);
|
||||
|
||||
try {
|
||||
// Check if group already exists
|
||||
@@ -407,7 +407,7 @@ piRestRouter.get('/pi/groups', async (ctx: Context) => {
|
||||
return ctx.json({ error: 'Unauthorized' }, 401);
|
||||
}
|
||||
|
||||
const userHome = getHomeDir(user.email);
|
||||
const userHome = getHomeDirForRole(user.email, user.role);
|
||||
|
||||
try {
|
||||
const groups = await storage.listGroups(userHome);
|
||||
@@ -447,7 +447,7 @@ piRestRouter.patch('/pi/groups/:groupSlug', async (ctx: Context) => {
|
||||
return ctx.json({ error: 'No valid updates provided' }, 400);
|
||||
}
|
||||
|
||||
const userHome = getHomeDir(user.email);
|
||||
const userHome = getHomeDirForRole(user.email, user.role);
|
||||
|
||||
try {
|
||||
const updatedGroup = await storage.updateGroupMeta(userHome, groupSlug, updates);
|
||||
@@ -476,7 +476,7 @@ piRestRouter.delete('/pi/groups/:groupSlug', async (ctx: Context) => {
|
||||
return ctx.json({ error: 'Group slug required' }, 400);
|
||||
}
|
||||
|
||||
const userHome = getHomeDir(user.email);
|
||||
const userHome = getHomeDirForRole(user.email, user.role);
|
||||
|
||||
try {
|
||||
await storage.deleteGroup(userHome, groupSlug);
|
||||
@@ -509,7 +509,7 @@ piRestRouter.post('/pi/sessions/:sessionId/move', async (ctx: Context) => {
|
||||
return ctx.json({ error: 'groupSlug must be a string or null' }, 400);
|
||||
}
|
||||
|
||||
const userHome = getHomeDir(user.email);
|
||||
const userHome = getHomeDirForRole(user.email, user.role);
|
||||
|
||||
try {
|
||||
// Find the session in root or any group
|
||||
|
||||
@@ -5,9 +5,8 @@ import { sessionManager } from './session-manager';
|
||||
import * as storage from './storage';
|
||||
import * as piBridge from './pi-bridge';
|
||||
import { sendClaudeCodeStreaming } from '@@/channels/send-claude-code';
|
||||
import { join, resolve } from 'path';
|
||||
import { homedir } from 'os';
|
||||
import { getHomeDir } from '../../../servers/data-path';
|
||||
import { join } from 'path';
|
||||
import { getHomeDirForRole } from '../../../servers/data-path';
|
||||
import { getUserSettings } from 'officerdb';
|
||||
import { logger } from './logger';
|
||||
|
||||
@@ -36,25 +35,16 @@ type WSData = {
|
||||
|
||||
const IDLE_TIMEOUT_MS = 60 * 60 * 1000; // 1 hour
|
||||
|
||||
const resolveSandboxedCwd = (email: string, cwdRoot?: string, cwd?: string) => {
|
||||
const root = !cwdRoot || cwdRoot === 'home' ? getHomeDir(email) : getHomeDir(email);
|
||||
const resolveCwd = (email: string, role: string, cwd?: string) => {
|
||||
const root = getHomeDirForRole(email, role);
|
||||
if (!cwd || cwd === '~') return root;
|
||||
if (cwd.startsWith('~/')) return join(root, cwd.slice(2));
|
||||
if (cwd.startsWith('/')) return join(root, cwd.slice(1));
|
||||
return root;
|
||||
};
|
||||
|
||||
const resolveHostCwd = (cwdRoot?: string, cwd?: string) => {
|
||||
if (cwdRoot === 'officer.dev') return resolve(process.cwd(), '..');
|
||||
const root = homedir();
|
||||
if (!cwd || cwd === '~') return root;
|
||||
if (cwd.startsWith('/')) return cwd;
|
||||
if (cwd.startsWith('~/')) return join(root, cwd.slice(2));
|
||||
return join(root, cwd);
|
||||
};
|
||||
|
||||
export const resolveBaseCwd = (email: string, cwdRoot?: string, cwd?: string) => {
|
||||
return resolveHostCwd(cwdRoot, cwd);
|
||||
export const resolveBaseCwd = (email: string, role: string, cwd?: string) => {
|
||||
return resolveCwd(email, role, cwd);
|
||||
};
|
||||
|
||||
const wsToSessionMap = new WeakMap<any, string>();
|
||||
@@ -289,10 +279,8 @@ async function handleChat(
|
||||
return handleClaudeCodeChat(ws, sessionId, model, msg);
|
||||
}
|
||||
|
||||
const homeDir = getHomeDir(email);
|
||||
const cwd = ws.data.sandboxed
|
||||
? resolveSandboxedCwd(email, msg.cwdRoot, msg.cwd)
|
||||
: resolveHostCwd(msg.cwdRoot, msg.cwd);
|
||||
const homeDir = getHomeDirForRole(email, ws.data.role);
|
||||
const cwd = resolveCwd(email, ws.data.role, msg.cwd);
|
||||
const groupSlug = msg.groupSlug || null;
|
||||
const session = sessionManager.getOrCreate(sessionId, email, cwd, model, groupSlug, msg.context, msg.contextId);
|
||||
session.userId = userId;
|
||||
@@ -312,15 +300,15 @@ async function handleChat(
|
||||
const onEvent = createEventHandler(sessionId, model, cwd, homeDir);
|
||||
|
||||
// If session has history, save to disk and pass --session for context replay
|
||||
let spawnOptions: { sessionFile?: string; username?: string } | undefined;
|
||||
let spawnOptions: { sessionFile?: string; username?: string; role?: string } | undefined;
|
||||
if (session.messages.length > 0) {
|
||||
await storage.saveSession(homeDir, sessionId, session.meta, session.messages);
|
||||
const hostPath = await storage.getSessionFilePath(homeDir, sessionId);
|
||||
if (hostPath) {
|
||||
spawnOptions = { sessionFile: hostPath, username };
|
||||
spawnOptions = { sessionFile: hostPath, username, role: ws.data.role };
|
||||
}
|
||||
}
|
||||
if (!spawnOptions) spawnOptions = { username };
|
||||
if (!spawnOptions) spawnOptions = { username, role: ws.data.role };
|
||||
|
||||
session.piProcess = await piBridge.spawnPi(cwd, model, userId, email, onEvent, spawnOptions);
|
||||
|
||||
@@ -389,11 +377,9 @@ async function handleClaudeCodeChat(
|
||||
},
|
||||
): Promise<void> {
|
||||
const { email, username, userId } = ws.data;
|
||||
const homeDir = getHomeDir(email);
|
||||
const homeDir = getHomeDirForRole(email, ws.data.role);
|
||||
|
||||
const cwd = ws.data.sandboxed
|
||||
? resolveSandboxedCwd(email, msg.cwdRoot, msg.cwd)
|
||||
: resolveHostCwd(msg.cwdRoot, msg.cwd);
|
||||
const cwd = resolveCwd(email, ws.data.role, msg.cwd);
|
||||
|
||||
const groupSlug = msg.groupSlug || null;
|
||||
|
||||
@@ -468,7 +454,7 @@ async function handleResume(
|
||||
let session = sessionManager.getSession(sessionId);
|
||||
|
||||
if (!session) {
|
||||
const homeDir = getHomeDir(email);
|
||||
const homeDir = getHomeDirForRole(email, ws.data.role);
|
||||
|
||||
try {
|
||||
const { meta, messages } = await storage.loadSession(homeDir, sessionId);
|
||||
@@ -500,19 +486,19 @@ async function handleResume(
|
||||
// Spawn fresh Pi process if needed
|
||||
if (!session.piProcess) {
|
||||
try {
|
||||
const homeDir = getHomeDir(email);
|
||||
const homeDir = getHomeDirForRole(email, ws.data.role);
|
||||
const onEvent = createEventHandler(sessionId, session.model, session.cwd, homeDir);
|
||||
|
||||
// If session has history, save to disk and pass --session for context replay
|
||||
let spawnOptions: { sessionFile?: string; username?: string } | undefined;
|
||||
let spawnOptions: { sessionFile?: string; username?: string; role?: string } | undefined;
|
||||
if (session.messages.length > 0) {
|
||||
await storage.saveSession(homeDir, sessionId, session.meta, session.messages);
|
||||
const hostPath = await storage.getSessionFilePath(homeDir, sessionId);
|
||||
if (hostPath) {
|
||||
spawnOptions = { sessionFile: hostPath, username: ws.data.username };
|
||||
spawnOptions = { sessionFile: hostPath, username: ws.data.username, role: ws.data.role };
|
||||
}
|
||||
}
|
||||
if (!spawnOptions) spawnOptions = { username: ws.data.username };
|
||||
if (!spawnOptions) spawnOptions = { username: ws.data.username, role: ws.data.role };
|
||||
|
||||
session.piProcess = await piBridge.spawnPi(
|
||||
session.cwd,
|
||||
|
||||
@@ -4,7 +4,7 @@ import type { PiEvent, MessageCost, Message } from '@@/api/pi/types';
|
||||
import { sessionManager } from '@@/api/pi/session-manager';
|
||||
import * as storage from '@@/api/pi/storage';
|
||||
import * as piBridge from '@@/api/pi/pi-bridge';
|
||||
import { getHomeDir } from '@@/data-path';
|
||||
import { getHomeDirForRole } from '@@/data-path';
|
||||
import { getUserSettings } from 'officerdb';
|
||||
import { logger } from '@@/api/pi/logger';
|
||||
import { sendClaudeCode, clearClaudeCodeSession } from './send-claude-code';
|
||||
@@ -21,6 +21,7 @@ type SendAndAwaitParams = {
|
||||
context: string;
|
||||
contextId: string;
|
||||
model?: string;
|
||||
role?: string;
|
||||
};
|
||||
|
||||
type SendAndAwaitResult = {
|
||||
@@ -132,7 +133,7 @@ function createDispatcher(sessionId: string): (event: PiEvent) => void {
|
||||
|
||||
async function doSend(sessionId: string, params: SendAndAwaitParams): Promise<SendAndAwaitResult> {
|
||||
const { userId, email, username, prompt, context, contextId } = params;
|
||||
const homeDir = getHomeDir(email);
|
||||
const homeDir = getHomeDirForRole(email, params.role ?? null);
|
||||
const cwd = homeDir;
|
||||
|
||||
// Resolve model: explicit param > !model override > user default > existing session > system default
|
||||
@@ -294,15 +295,16 @@ async function doSend(sessionId: string, params: SendAndAwaitParams): Promise<Se
|
||||
(async () => {
|
||||
try {
|
||||
if (!session.piProcess) {
|
||||
let spawnOptions: { sessionFile?: string; username?: string } | undefined;
|
||||
const role = params.role;
|
||||
let spawnOptions: { sessionFile?: string; username?: string; role?: string } | undefined;
|
||||
if (session.messages.length > 0) {
|
||||
await storage.saveSession(homeDir, sessionId, session.meta, session.messages);
|
||||
const hostPath = await storage.getSessionFilePath(homeDir, sessionId);
|
||||
if (hostPath) {
|
||||
spawnOptions = { sessionFile: hostPath, username };
|
||||
spawnOptions = { sessionFile: hostPath, username, role };
|
||||
}
|
||||
}
|
||||
if (!spawnOptions) spawnOptions = { username };
|
||||
if (!spawnOptions) spawnOptions = { username, role };
|
||||
|
||||
const dispatcher = createDispatcher(sessionId);
|
||||
session.piProcess = await piBridge.spawnPi(cwd, model!, userId, email, dispatcher, spawnOptions);
|
||||
|
||||
@@ -31,6 +31,9 @@ export const getArchivedSessionDir = (email: string, sessionId: string) =>
|
||||
|
||||
export const getHomeDir = (email: string) => join(DATA_PATH, email, 'home');
|
||||
|
||||
export const getHomeDirForRole = (email: string, role: string | null): string =>
|
||||
role === 'Super Admin' && process.env.HOME_DIR ? process.env.HOME_DIR : getHomeDir(email);
|
||||
|
||||
export const getUserPiConfigDir = (email: string) => join(DATA_PATH, email, 'home', '.pi', 'agent');
|
||||
|
||||
export const getUserSettingsDir = (email: string) => join(DATA_PATH, email, 'settings');
|
||||
|
||||
Reference in New Issue
Block a user