Pi running inside the docker containers
This commit is contained in:
@@ -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<Subprocess> {
|
||||
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<Uint8Array>;
|
||||
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<Uint8Array>;
|
||||
const stderrReader = stderr.getReader();
|
||||
const stderrDecoder = new TextDecoder();
|
||||
(async () => {
|
||||
try {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -220,9 +220,9 @@ function createEventHandler(sessionId: string, model: string, cwd: string) {
|
||||
|
||||
async function handleChat(
|
||||
ws: ServerWebSocket<WSData>,
|
||||
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<void> {
|
||||
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' });
|
||||
|
||||
Reference in New Issue
Block a user