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:
Gabriel Brown
2026-06-24 09:51:39 -04:00
parent 683fc62129
commit 4c0de2cbf3
5 changed files with 224 additions and 33 deletions
+18 -9
View File
@@ -80,18 +80,23 @@ export const containerVolumeSuffix = () =>
export { hostWorkspacePath };
export const jobWorkspaceVolumeSpec = (workdir: string) => {
export const jobWorkspaceVolumeSpec = (
workdir: string,
containerHome = '/workspace',
) => {
const volumeOptions =
env.containerVolumeOptions ??
(containerRuntime().endsWith('podman') ? 'Z' : undefined);
const source = hostWorkspacePath(workdir);
return volumeOptions
? `${source}:/workspace:${volumeOptions}`
: `${source}:/workspace`;
? `${source}:${containerHome}:${volumeOptions}`
: `${source}:${containerHome}`;
};
export const runInJobContainer = async (args: {
workdir: string;
containerHome?: string;
containerCwd?: string;
command: string[];
environment: Record<string, string>;
redact: (value: string) => string;
@@ -110,9 +115,9 @@ export const runInJobContainer = async (args: {
...networkArgs(),
...environmentArgs(args.environment),
'-v',
jobWorkspaceVolumeSpec(args.workdir),
jobWorkspaceVolumeSpec(args.workdir, args.containerHome),
'-w',
'/workspace/repo',
args.containerCwd ?? '/workspace/repo',
env.jobImage,
...args.command,
],
@@ -128,6 +133,8 @@ export const runInJobContainer = async (args: {
export const startWorkspaceContainer = async (args: {
workdir: string;
containerHome?: string;
containerCwd?: string;
containerName: string;
environment: Record<string, string>;
command?: string[];
@@ -154,9 +161,9 @@ export const startWorkspaceContainer = async (args: {
: []),
...environmentArgs(args.environment),
'-v',
jobWorkspaceVolumeSpec(args.workdir),
jobWorkspaceVolumeSpec(args.workdir, args.containerHome),
'-w',
'/workspace/repo',
args.containerCwd ?? '/workspace/repo',
env.jobImage,
...(args.command ?? ['sleep', 'infinity']),
],
@@ -220,6 +227,8 @@ export const execInWorkspaceContainer = async (args: {
export const streamInJobContainer = async (args: {
workdir: string;
containerHome?: string;
containerCwd?: string;
command: string[];
environment: Record<string, string>;
redact: (value: string) => string;
@@ -240,9 +249,9 @@ export const streamInJobContainer = async (args: {
...networkArgs(),
...environmentArgs(args.environment),
'-v',
jobWorkspaceVolumeSpec(args.workdir),
jobWorkspaceVolumeSpec(args.workdir, args.containerHome),
'-w',
'/workspace/repo',
args.containerCwd ?? '/workspace/repo',
env.jobImage,
...args.command,
],