Worker: persistent per-user home + dotfiles materialization
- Each job/terminal now mounts a persistent per-user home (${workdir}/homes/
{username}) at /home/{username}; the thread checkout lives at
~/Code/{spoon}/{branch} so every thread shows up as a folder in one home and
dotfiles/tools/nvim plugins persist across sessions
- docker.ts helpers + git.ts cloneRepository take container home/cwd + dir name
(backward-compatible defaults); codex/opencode/terminal use the per-user paths
- new user-environment.ts: fetchUserEnvironment (worker-token Convex action) +
materializeUserHome — ensures ~/.bash_profile, applies the editable overlay
files, and (hashed/idempotent) clones the public dotfiles repo + runs the
setup command inside the job image
- stopWorkspace no longer deletes the home; only the container stops
- Verified: codex runs a real turn under the new /home/{user} + ~/Code layout;
overlay .bashrc loads in the interactive shell
This commit is contained in:
@@ -11,7 +11,6 @@ import { getTerminalWorkspace } from './worker';
|
||||
|
||||
const TERMINAL_IMAGE = env.terminalImage;
|
||||
const IDLE_STOP_MS = env.terminalIdleMs;
|
||||
const CONTAINER_WORKDIR = '/workspace/repo';
|
||||
|
||||
const docker = new Docker();
|
||||
|
||||
@@ -21,7 +20,11 @@ const containerName = (jobId: string) =>
|
||||
type Session = { connections: number; idleTimer?: NodeJS.Timeout };
|
||||
const sessions = new Map<string, Session>();
|
||||
|
||||
const ensureTerminalContainer = async (jobId: string, workdir: string) => {
|
||||
const ensureTerminalContainer = async (
|
||||
jobId: string,
|
||||
workdir: string,
|
||||
containerHome: string,
|
||||
) => {
|
||||
const name = containerName(jobId);
|
||||
const container = docker.getContainer(name);
|
||||
const info = await container.inspect().catch(() => null);
|
||||
@@ -35,11 +38,11 @@ const ensureTerminalContainer = async (jobId: string, workdir: string) => {
|
||||
name,
|
||||
Image: TERMINAL_IMAGE,
|
||||
Cmd: ['sleep', 'infinity'],
|
||||
WorkingDir: CONTAINER_WORKDIR,
|
||||
WorkingDir: containerHome,
|
||||
Tty: false,
|
||||
Labels: { 'spoon.agent.terminal': jobId },
|
||||
HostConfig: {
|
||||
Binds: [`${source}:/workspace${suffix ? `:${suffix}` : ''}`],
|
||||
Binds: [`${source}:${containerHome}${suffix ? `:${suffix}` : ''}`],
|
||||
NetworkMode: env.network,
|
||||
Memory: 4 * 1024 * 1024 * 1024,
|
||||
AutoRemove: false,
|
||||
@@ -81,7 +84,11 @@ const bridge = async (ws: WebSocket, jobId: string) => {
|
||||
let stream: Duplex | undefined;
|
||||
let exec: Docker.Exec | undefined;
|
||||
try {
|
||||
const container = await ensureTerminalContainer(jobId, workspace.workdir);
|
||||
const container = await ensureTerminalContainer(
|
||||
jobId,
|
||||
workspace.workdir,
|
||||
workspace.containerHome,
|
||||
);
|
||||
exec = await container.exec({
|
||||
// Reattach a persistent tmux session across reconnects when tmux is
|
||||
// available; otherwise fall back to a plain login shell.
|
||||
@@ -94,9 +101,10 @@ const bridge = async (ws: WebSocket, jobId: string) => {
|
||||
AttachStdout: true,
|
||||
AttachStderr: true,
|
||||
Tty: true,
|
||||
WorkingDir: CONTAINER_WORKDIR,
|
||||
WorkingDir: workspace.containerRepo,
|
||||
Env: [
|
||||
'TERM=xterm-256color',
|
||||
`HOME=${workspace.containerHome}`,
|
||||
...workspace.secrets.map((s) => `${s.name}=${s.value}`),
|
||||
],
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user