diff --git a/docker/agent.Dockerfile b/docker/agent.Dockerfile index 36213aaa..1aa1a758 100644 --- a/docker/agent.Dockerfile +++ b/docker/agent.Dockerfile @@ -80,6 +80,22 @@ RUN npm install -g @deepseek-ai/dsh \ && npm cache clean --force \ && dsh --version +# OMP (Oh My Pi) is NOT on npm: a standalone binary via omp.sh's installer, which +# targets $HOME/.local/bin with no --dir override (verified 2026-08-27 — the +# resolver's OMP_SEARCH_DIRS lists ~/.omp/bin first, which turned out to be the +# WRONG guess for the installer's actual target; build this step for real +# rather than trust that ordering). At build time $HOME is root's home and +# unreachable by the `agent` user, so copy the binary into /usr/local/bin and +# drop root's ~/.local/bin/omp in the same layer so the image does not carry +# the download twice. +RUN curl -fsSL https://omp.sh/install | sh \ + && cp -L /root/.local/bin/omp /usr/local/bin/omp.real \ + && rm -f /usr/local/bin/omp \ + && mv /usr/local/bin/omp.real /usr/local/bin/omp \ + && chmod 755 /usr/local/bin/omp \ + && rm -f /root/.local/bin/omp \ + && omp --version + # `agent` user (gid 0) with an arbitrary-uid-writable HOME. The uid is # auto-assigned (node:22-slim already occupies uid 1000 with its `node` user); at # runtime Codeman overrides with `--user :0` on Linux, so the baked uid @@ -106,10 +122,14 @@ ENV HOME=/home/agent # writable by the arbitrary uid the container actually runs as, and a profile # installed after it would miss that fixup. DSH_HOME points the launcher at the # agent's dir while this still runs as root. +# `.omp/agent` is pre-created for the same reason `.codex` is: it is a MIXED +# store (per-file config seeds PLUS a shared `sessions/` RW bind mount for +# Codeman's own host-side history/resume reads), and neither kind of artifact +# creates its own parent directory. RUN useradd -g 0 -m -d /home/agent -s /bin/bash agent \ && mkdir -p /home/agent/.npm /home/agent/.cache /home/agent/.config /home/agent/.codeman \ /home/agent/.claude/projects /home/agent/.codex/sessions /home/agent/.pi/agent /home/agent/.grok \ - /home/agent/.dsh \ + /home/agent/.dsh /home/agent/.omp/agent \ && DSH_HOME=/home/agent/.dsh HOME=/home/agent \ dsh plugin --profile dsh-tui add @deepseek-harness-tui/dsh-tui \ && test -f /home/agent/.dsh/profiles/dsh-tui/package.json \ diff --git a/src/docker-hosts.ts b/src/docker-hosts.ts index 772bc9aa..ac4ffcd3 100644 --- a/src/docker-hosts.ts +++ b/src/docker-hosts.ts @@ -640,6 +640,22 @@ const CRED_STORES: CredStorePolicy[] = [ }, { rel: '.config/gcloud', seedWhole: true }, { rel: '.config/opencode', seedWhole: true }, + // OMP keeps its config in `~/.omp/agent` (config.yml/mcp.json/models.yml/ + // settings.yml — small, no bigger than grok's config.toml/pager.toml), but + // that dir ALSO holds agent.db/history.db/models.db (SQLite caches) and + // terminal-sessions/blobs/cache (large, regenerable), so seed only the + // config files. UNLIKE pi/grok, `sessions/` is SHARED (RW), not + // host-invisible: Codeman reads `~/.omp/agent/sessions/**/*.jsonl` + // HOST-SIDE for history recovery and --resume pinning + // (omp-transcript.ts, omp-session-resolver.ts) — the same reason codex's + // `sessions/` is shared rather than seeded. Without this, an in-container + // OMP conversation would be invisible to Codeman's own history-scan/resume + // logic, silently breaking the kill-survival feature for Docker cases. + { + rel: '.omp/agent', + shareDirs: ['sessions'], + seedFiles: ['config.yml', 'mcp.json', 'models.yml', 'settings.yml'], + }, ]; /** diff --git a/test/docker-hosts.test.ts b/test/docker-hosts.test.ts index 3d848433..f32afe76 100644 --- a/test/docker-hosts.test.ts +++ b/test/docker-hosts.test.ts @@ -329,6 +329,32 @@ describe('resolveDockerCredentialArtifacts (isolated codex/gemini/gcloud/opencod expect(mounts).toEqual([]); expect(seedCopies).toEqual([]); }); + + it('omp: shares sessions/ RW (host-side history/resume reads), seeds config files only', () => { + mkdirSync(join(home, '.omp', 'agent', 'sessions'), { recursive: true }); + writeFileSync(join(home, '.omp', 'agent', 'config.yml'), ''); + writeFileSync(join(home, '.omp', 'agent', 'mcp.json'), '{}'); + writeFileSync(join(home, '.omp', 'agent', 'models.yml'), ''); + writeFileSync(join(home, '.omp', 'agent', 'settings.yml'), ''); + // Regenerable local state that must NOT be seeded (mirrors the pi/grok exclusions). + writeFileSync(join(home, '.omp', 'agent', 'agent.db'), ''); + mkdirSync(join(home, '.omp', 'agent', 'terminal-sessions'), { recursive: true }); + + const { mounts, seedCopies } = resolveDockerCredentialArtifacts(home); + expect(mounts).toContainEqual({ + src: join(home, '.omp', 'agent', 'sessions'), + dst: '/home/agent/.omp/agent/sessions', + }); + const dests = seedCopies.map((s) => s.to); + expect(dests).toContain('/home/agent/.omp/agent/config.yml'); + expect(dests).toContain('/home/agent/.omp/agent/mcp.json'); + expect(dests).toContain('/home/agent/.omp/agent/models.yml'); + expect(dests).toContain('/home/agent/.omp/agent/settings.yml'); + expect(dests).not.toContain('/home/agent/.omp/agent/agent.db'); + expect(mounts.some((m) => m.dst === '/home/agent/.omp/agent/terminal-sessions')).toBe(false); + // seed copies of individual files are NOT recursive + expect(seedCopies.filter((s) => s.to.startsWith('/home/agent/.omp')).every((s) => !s.recursive)).toBe(true); + }); }); describe('resolveDockerClaudeArtifacts (isolated claude state)', () => {