aicodeman 1.3.5 → 1.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -0
- package/dist/docker-export.d.ts +107 -0
- package/dist/docker-export.d.ts.map +1 -0
- package/dist/docker-export.js +335 -0
- package/dist/docker-export.js.map +1 -0
- package/dist/docker-hosts.d.ts +264 -0
- package/dist/docker-hosts.d.ts.map +1 -0
- package/dist/docker-hosts.js +744 -0
- package/dist/docker-hosts.js.map +1 -0
- package/dist/mux-interface.d.ts +7 -1
- package/dist/mux-interface.d.ts.map +1 -1
- package/dist/session.d.ts +9 -2
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +48 -4
- package/dist/session.js.map +1 -1
- package/dist/tmux-manager.d.ts +58 -1
- package/dist/tmux-manager.d.ts.map +1 -1
- package/dist/tmux-manager.js +247 -7
- package/dist/tmux-manager.js.map +1 -1
- package/dist/types/api.d.ts +9 -1
- package/dist/types/api.d.ts.map +1 -1
- package/dist/types/api.js.map +1 -1
- package/dist/types/session.d.ts +102 -0
- package/dist/types/session.d.ts.map +1 -1
- package/dist/types/session.js.map +1 -1
- package/dist/web/network-auth-policy.d.ts +9 -0
- package/dist/web/network-auth-policy.d.ts.map +1 -1
- package/dist/web/network-auth-policy.js +12 -0
- package/dist/web/network-auth-policy.js.map +1 -1
- package/dist/web/public/api-client.c9b1cddc.js.gz +0 -0
- package/dist/web/public/{app.6890f3d0.js → app.6c4b9893.js} +1 -1
- package/dist/web/public/app.6c4b9893.js.br +0 -0
- package/dist/web/public/app.6c4b9893.js.gz +0 -0
- package/dist/web/public/{constants.5b18ed64.js → constants.48da7f03.js} +7 -0
- package/dist/web/public/constants.48da7f03.js.br +0 -0
- package/dist/web/public/{constants.5b18ed64.js.gz → constants.48da7f03.js.gz} +0 -0
- package/dist/web/public/cron-ui.js.gz +0 -0
- package/dist/web/public/image-input.ee16ad88.js.gz +0 -0
- package/dist/web/public/index.html +148 -11
- package/dist/web/public/index.html.br +0 -0
- package/dist/web/public/index.html.gz +0 -0
- package/dist/web/public/input-cjk.63794d0b.js.gz +0 -0
- package/dist/web/public/keyboard-accessory.3f91acef.js.gz +0 -0
- package/dist/web/public/mobile-handlers.b5a667ef.js.gz +0 -0
- package/dist/web/public/mobile.0644e3ac.css.gz +0 -0
- package/dist/web/public/notification-manager.ca9dc40d.js.gz +0 -0
- package/dist/web/public/orchestrator-panel.js.gz +0 -0
- package/dist/web/public/{panels-ui.ca03853f.js → panels-ui.3c9b0465.js} +53 -53
- package/dist/web/public/panels-ui.3c9b0465.js.br +0 -0
- package/dist/web/public/panels-ui.3c9b0465.js.gz +0 -0
- package/dist/web/public/ralph-panel.6de2d0f8.js.gz +0 -0
- package/dist/web/public/ralph-wizard.13a1831e.js.gz +0 -0
- package/dist/web/public/respawn-ui.2d249da9.js.gz +0 -0
- package/dist/web/public/sanitize-html.bc7078d6.js.gz +0 -0
- package/dist/web/public/session-ui.4851c61a.js +58 -0
- package/dist/web/public/session-ui.4851c61a.js.br +0 -0
- package/dist/web/public/session-ui.4851c61a.js.gz +0 -0
- package/dist/web/public/settings-ui.29214cf1.js +67 -0
- package/dist/web/public/settings-ui.29214cf1.js.br +0 -0
- package/dist/web/public/settings-ui.29214cf1.js.gz +0 -0
- package/dist/web/public/{styles.3d78fd0d.css → styles.ff43f80f.css} +1 -1
- package/dist/web/public/styles.ff43f80f.css.br +0 -0
- package/dist/web/public/styles.ff43f80f.css.gz +0 -0
- package/dist/web/public/subagent-windows.69b60c35.js.gz +0 -0
- package/dist/web/public/sw.js.gz +0 -0
- package/dist/web/public/terminal-ui.efda6083.js.gz +0 -0
- package/dist/web/public/ultracode-panel.js.gz +0 -0
- package/dist/web/public/ultracode-windows.js.gz +0 -0
- package/dist/web/public/upload.html.gz +0 -0
- package/dist/web/public/vendor/dompurify.min.js.gz +0 -0
- package/dist/web/public/vendor/marked.min.js.gz +0 -0
- package/dist/web/public/vendor/xterm-addon-fit.min.js.gz +0 -0
- package/dist/web/public/vendor/xterm-addon-serialize.min.js.gz +0 -0
- package/dist/web/public/vendor/xterm-addon-unicode11.min.js.gz +0 -0
- package/dist/web/public/vendor/xterm-addon-webgl.min.js.gz +0 -0
- package/dist/web/public/vendor/xterm-zerolag-input.137ad9f0.js.gz +0 -0
- package/dist/web/public/vendor/xterm.css.gz +0 -0
- package/dist/web/public/vendor/xterm.min.js.gz +0 -0
- package/dist/web/public/voice-input.085e9e73.js.gz +0 -0
- package/dist/web/routes/case-routes.d.ts.map +1 -1
- package/dist/web/routes/case-routes.js +422 -3
- package/dist/web/routes/case-routes.js.map +1 -1
- package/dist/web/routes/session-routes.d.ts.map +1 -1
- package/dist/web/routes/session-routes.js +85 -5
- package/dist/web/routes/session-routes.js.map +1 -1
- package/dist/web/schemas.d.ts +72 -1
- package/dist/web/schemas.d.ts.map +1 -1
- package/dist/web/schemas.js +158 -0
- package/dist/web/schemas.js.map +1 -1
- package/dist/web/server.d.ts +11 -0
- package/dist/web/server.d.ts.map +1 -1
- package/dist/web/server.js +81 -1
- package/dist/web/server.js.map +1 -1
- package/dist/web/sse-events.d.ts +21 -0
- package/dist/web/sse-events.d.ts.map +1 -1
- package/dist/web/sse-events.js +23 -0
- package/dist/web/sse-events.js.map +1 -1
- package/package.json +1 -1
- package/dist/web/public/app.6890f3d0.js.br +0 -0
- package/dist/web/public/app.6890f3d0.js.gz +0 -0
- package/dist/web/public/constants.5b18ed64.js.br +0 -0
- package/dist/web/public/panels-ui.ca03853f.js.br +0 -0
- package/dist/web/public/panels-ui.ca03853f.js.gz +0 -0
- package/dist/web/public/session-ui.c9094fe9.js +0 -49
- package/dist/web/public/session-ui.c9094fe9.js.br +0 -0
- package/dist/web/public/session-ui.c9094fe9.js.gz +0 -0
- package/dist/web/public/settings-ui.9538a0c1.js +0 -67
- package/dist/web/public/settings-ui.9538a0c1.js.br +0 -0
- package/dist/web/public/settings-ui.9538a0c1.js.gz +0 -0
- package/dist/web/public/styles.3d78fd0d.css.br +0 -0
- package/dist/web/public/styles.3d78fd0d.css.gz +0 -0
package/dist/tmux-manager.js
CHANGED
|
@@ -27,10 +27,12 @@ const execAsync = promisify(exec);
|
|
|
27
27
|
import { existsSync, readFileSync, mkdirSync } from 'node:fs';
|
|
28
28
|
import { writeFile, rename } from 'node:fs/promises';
|
|
29
29
|
import { dirname } from 'node:path';
|
|
30
|
-
import {
|
|
30
|
+
import { homedir } from 'node:os';
|
|
31
|
+
import { dataPath, DEFAULT_TMUX_SOCKET, CODEMAN_INSTANCE } from './config/instance.js';
|
|
31
32
|
import { getErrorMessage, DEFAULT_NICE_CONFIG, } from './types.js';
|
|
32
33
|
import { buildEffortCliArgs } from './session-cli-builder.js';
|
|
33
34
|
import { buildSshConnectionArgs, defaultRemoteCommandForMode, remoteSshTarget } from './remote-hosts.js';
|
|
35
|
+
import { buildDockerBaseArgs, buildDockerCreateArgs, containerApiUrl, CONTAINER_HOME, defaultDockerCommandForMode, hostGatewayAlias, resolveDockerClaudeArtifacts, resolveDockerCredentialArtifacts, } from './docker-hosts.js';
|
|
34
36
|
import { wrapWithNice, SAFE_PATH_PATTERN, findClaudeDir, resolveOpenCodeDir, resolveCodexDir, resolveGeminiDir, } from './utils/index.js';
|
|
35
37
|
// ============================================================================
|
|
36
38
|
// Timing Constants
|
|
@@ -700,6 +702,222 @@ export function buildRemoteKillCommand(options) {
|
|
|
700
702
|
const [ssh, ...connectionArgs] = buildSshConnectionArgs(remote);
|
|
701
703
|
return [ssh, ...connectionArgs, remoteSshTarget(remote), shellescape(killCmd)].join(' ');
|
|
702
704
|
}
|
|
705
|
+
// ========== Docker cases (COD-Docker) ==========
|
|
706
|
+
//
|
|
707
|
+
// The docker analog of the remote-SSH launch above. Instead of a local tmux pane
|
|
708
|
+
// running `ssh -t host 'tmux new-session …'`, it runs `docker exec -it <container>
|
|
709
|
+
// sh -lc 'tmux new-session …'` into a DURABLE in-container tmux server. The
|
|
710
|
+
// container is per-CASE, so many sessions `docker exec` into the same one. See
|
|
711
|
+
// docs/docker-cases-plan.md.
|
|
712
|
+
/**
|
|
713
|
+
* DEDICATED in-container tmux socket. A Codeman running INSIDE the container uses
|
|
714
|
+
* `-L codeman`; ours is `-L codeman-docker` with a `codeman-dkr-*` session name
|
|
715
|
+
* that deliberately FAILS SAFE_MUX_NAME_PATTERN, so an in-container Codeman never
|
|
716
|
+
* adopts/resizes/respawns our session (same defence as the remote socket).
|
|
717
|
+
*/
|
|
718
|
+
const DOCKER_TMUX_SOCKET = 'codeman-docker';
|
|
719
|
+
/**
|
|
720
|
+
* Deterministic, reattach-stable in-container tmux session name. Derived from the
|
|
721
|
+
* same stable field the local muxName uses (first 8 chars of the sessionId), so a
|
|
722
|
+
* reconnect re-issues the exact same `new-session -A` and lands back in the SAME
|
|
723
|
+
* in-container session. The `dkr` letters make it fail SAFE_MUX_NAME_PATTERN.
|
|
724
|
+
*/
|
|
725
|
+
export function dockerTmuxSessionName(sessionId) {
|
|
726
|
+
return `codeman-dkr-${sessionId.slice(0, 8)}`;
|
|
727
|
+
}
|
|
728
|
+
/** Resume ids are UUID-ish; reject anything with shell metacharacters (defensive). */
|
|
729
|
+
const RESUME_ID_SAFE = /^[A-Za-z0-9._-]+$/;
|
|
730
|
+
/**
|
|
731
|
+
* Append the CLI-specific resume flag to a pane command. Only fires when the
|
|
732
|
+
* in-container tmux is RE-CREATED (`new-session -A` makes the flag inert on a
|
|
733
|
+
* live reattach), i.e. exactly when the previous live agent was lost and we want
|
|
734
|
+
* to resume the conversation from the bind-mounted transcript.
|
|
735
|
+
*/
|
|
736
|
+
function appendResumeFlag(modeCommand, mode, resumeId) {
|
|
737
|
+
if (!RESUME_ID_SAFE.test(resumeId))
|
|
738
|
+
return modeCommand;
|
|
739
|
+
switch (mode) {
|
|
740
|
+
case 'claude':
|
|
741
|
+
case 'gemini':
|
|
742
|
+
return `${modeCommand} --resume ${resumeId}`;
|
|
743
|
+
case 'codex':
|
|
744
|
+
return `${modeCommand} resume ${resumeId}`;
|
|
745
|
+
default:
|
|
746
|
+
return modeCommand; // shell / opencode: no resume
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
/**
|
|
750
|
+
* Build the ONE `bash -c` launch string for a docker session: image-check ->
|
|
751
|
+
* ensure (inspect-or-create) -> start -> `exec docker exec -it` into the durable
|
|
752
|
+
* in-container tmux (resume-aware). PURE and unit-testable. The escaping survives
|
|
753
|
+
* four layers: outer `bash -c "…"` (JSON.stringify at respawn-pane) -> the joined
|
|
754
|
+
* command -> `docker exec … sh -lc '<tmux>'` -> tmux `'<paneCommand>'`.
|
|
755
|
+
*/
|
|
756
|
+
export function buildDockerLaunchCommand(opts) {
|
|
757
|
+
const { mode, docker, sessionId, resumeSessionId, createContext, execEnv, execEnvNames, seedCopies } = opts;
|
|
758
|
+
const base = buildDockerBaseArgs(docker).join(' ');
|
|
759
|
+
const createArgs = buildDockerCreateArgs(createContext).join(' ');
|
|
760
|
+
const name = shellescape(docker.containerName);
|
|
761
|
+
const workdir = shellescape(docker.containerWorkdir);
|
|
762
|
+
const image = shellescape(docker.image);
|
|
763
|
+
const dkrName = dockerTmuxSessionName(sessionId);
|
|
764
|
+
const sid = sessionId.slice(0, 8);
|
|
765
|
+
let modeCommand = docker.commands?.[mode] || defaultDockerCommandForMode(mode);
|
|
766
|
+
if (resumeSessionId)
|
|
767
|
+
modeCommand = appendResumeFlag(modeCommand, mode, resumeSessionId);
|
|
768
|
+
// Run by tmux via /bin/sh -c, so the path is shell-quoted here. `exec` makes the
|
|
769
|
+
// pane PID the agent itself.
|
|
770
|
+
const paneCommand = `cd ${workdir} && ${modeCommand}`;
|
|
771
|
+
// `setenv -g` primes the session id so reattaches / newly-created panes inherit
|
|
772
|
+
// it. `new-session -A` = attach-or-create (idempotent + resume-aware). Options
|
|
773
|
+
// are scoped per-session (`set -t`) or server (`set -s`), never `-g`, so a shared
|
|
774
|
+
// in-container tmux server's other sessions keep their own prefix/mouse.
|
|
775
|
+
const tmuxInvocation = [
|
|
776
|
+
`tmux -L ${DOCKER_TMUX_SOCKET} setenv -g CODEMAN_SESSION_ID ${shellescape(sid)}`,
|
|
777
|
+
'setenv -g CODEMAN_MUX 1',
|
|
778
|
+
`new-session -A -s ${dkrName} -c ${workdir} ${shellescape(paneCommand)}`,
|
|
779
|
+
`set -t ${dkrName} status off`,
|
|
780
|
+
`set -t ${dkrName} mouse off`,
|
|
781
|
+
`set -t ${dkrName} prefix C-q`,
|
|
782
|
+
'set -s escape-time 0',
|
|
783
|
+
].join(' \\; ');
|
|
784
|
+
const execEnvFlags = [];
|
|
785
|
+
for (const [k, v] of Object.entries(execEnv))
|
|
786
|
+
execEnvFlags.push('--env', shellescape(`${k}=${v}`));
|
|
787
|
+
// NAME-ONLY forwards: docker reads the VALUE from Codeman's own process env, so
|
|
788
|
+
// the secret never appears in argv (no `ps` leak) and is not committed.
|
|
789
|
+
for (const n of execEnvNames)
|
|
790
|
+
execEnvFlags.push('--env', n);
|
|
791
|
+
for (const extra of docker.extraExecArgs ?? [])
|
|
792
|
+
execEnvFlags.push(shellescape(extra));
|
|
793
|
+
const imageMissingMsg = shellescape(`Codeman: base image ${docker.image} not present (it is normally auto-built on first use)`);
|
|
794
|
+
const startFailMsg = shellescape(`Codeman: container ${docker.containerName} failed to start (docker daemon down?)`);
|
|
795
|
+
const imageCheck = `${base} image inspect ${image} >/dev/null 2>&1 || { echo ${imageMissingMsg}; exit 1; }`;
|
|
796
|
+
// create-if-missing (idempotent): reconnect / boot recovery re-runs this exact chain.
|
|
797
|
+
const ensure = `${base} inspect ${name} >/dev/null 2>&1 || ${base} ${createArgs}`;
|
|
798
|
+
const start = `${base} start ${name} >/dev/null 2>&1 || { echo ${startFailMsg}; exit 1; }`;
|
|
799
|
+
// Seed writable credential config from read-only host mounts ONCE per container
|
|
800
|
+
// (guarded by [ -e ] so reconnects never clobber in-container config; `cp -a` for
|
|
801
|
+
// whole-dir credential seeds). mkdir -p the parent so a file seed works even when
|
|
802
|
+
// no sibling share-mount pre-created the dir. Paths are fixed CONTAINER_HOME
|
|
803
|
+
// constants (no shell metachars), so the whole inner command is shell-quoted once.
|
|
804
|
+
const seedSteps = (seedCopies ?? []).map((s) => {
|
|
805
|
+
const cp = s.recursive ? 'cp -a' : 'cp';
|
|
806
|
+
const parent = s.to.slice(0, s.to.lastIndexOf('/'));
|
|
807
|
+
return `mkdir -p ${parent} 2>/dev/null; [ -e ${s.to} ] || ${cp} ${s.from} ${s.to} 2>/dev/null || true`;
|
|
808
|
+
});
|
|
809
|
+
const innerCmd = seedSteps.length ? `${seedSteps.join(' ; ')} ; ${tmuxInvocation}` : tmuxInvocation;
|
|
810
|
+
const execCmd = `exec ${base} exec -it --workdir ${workdir} ${execEnvFlags.join(' ')} ${name} sh -lc ${shellescape(innerCmd)}`;
|
|
811
|
+
return [imageCheck, ensure, start, execCmd].join(' ; ');
|
|
812
|
+
}
|
|
813
|
+
/**
|
|
814
|
+
* Kill ONLY this session's in-container tmux session. The container is shared by
|
|
815
|
+
* the case's other sessions, so this NEVER `docker stop`s it — stopping/removing
|
|
816
|
+
* the container is an explicit teardown (buildDockerStopCommand) or case-delete
|
|
817
|
+
* (buildDockerRemoveCommand). Fired best-effort on session kill.
|
|
818
|
+
*/
|
|
819
|
+
export function buildDockerKillCommand(options) {
|
|
820
|
+
const { docker, sessionId } = options;
|
|
821
|
+
const base = buildDockerBaseArgs(docker).join(' ');
|
|
822
|
+
const dkrName = dockerTmuxSessionName(sessionId);
|
|
823
|
+
return `${base} exec ${shellescape(docker.containerName)} tmux -L ${DOCKER_TMUX_SOCKET} kill-session -t ${shellescape(dkrName)}`;
|
|
824
|
+
}
|
|
825
|
+
/** Explicit container stop (frees RAM/CPU; conversation resumes on next launch via --resume). */
|
|
826
|
+
export function buildDockerStopCommand(docker) {
|
|
827
|
+
return `${buildDockerBaseArgs(docker).join(' ')} stop -t 10 ${shellescape(docker.containerName)}`;
|
|
828
|
+
}
|
|
829
|
+
/** Explicit container removal (case-delete). Destroys in-image state; bind mounts survive. */
|
|
830
|
+
export function buildDockerRemoveCommand(docker) {
|
|
831
|
+
return `${buildDockerBaseArgs(docker).join(' ')} rm -f ${shellescape(docker.containerName)}`;
|
|
832
|
+
}
|
|
833
|
+
/**
|
|
834
|
+
* Resolve the environment-dependent bits of a docker launch (host uid, existing
|
|
835
|
+
* credential mounts, derived api url, hook-secret mount, Desktop detection) into
|
|
836
|
+
* the pure buildDockerLaunchCommand inputs. IO; only ever called from the real
|
|
837
|
+
* launch path (createSession/respawnPane no-op under VITEST).
|
|
838
|
+
*/
|
|
839
|
+
export function resolveDockerLaunchOptions(mode, docker, sessionId, resumeSessionId) {
|
|
840
|
+
const home = homedir();
|
|
841
|
+
const isDesktop = process.platform === 'darwin'; // Docker Desktop translates uids + native host.docker.internal
|
|
842
|
+
const uid = typeof process.getuid === 'function' ? process.getuid() : 1000;
|
|
843
|
+
const userArgs = docker.engine === 'podman'
|
|
844
|
+
? ['--userns=keep-id'] // rootless podman: map host uid to the image `agent` uid
|
|
845
|
+
: isDesktop
|
|
846
|
+
? [] // Desktop: run as the image's baked uid (a mac uid wouldn't own /home/agent)
|
|
847
|
+
: ['--user', `${uid}:0`]; // Linux: host uid + GID 0 (OpenShift arbitrary-uid writable HOME)
|
|
848
|
+
const gatewayAlias = hostGatewayAlias(docker.engine);
|
|
849
|
+
const credentialMounts = [];
|
|
850
|
+
const extraMounts = [];
|
|
851
|
+
// Isolated credential state (Claude + codex/gemini/gcloud/opencode): each store
|
|
852
|
+
// shares ONLY what a host feature / --resume needs (Claude projects/, codex
|
|
853
|
+
// sessions/+history) and seeds everything else (tokens, settings, configs) as
|
|
854
|
+
// writable copies, so the container is authed WITHOUT re-auth and WITHOUT writing
|
|
855
|
+
// its runtime state back into the host dirs. Only when credentials are mounted.
|
|
856
|
+
let seedCopies = [];
|
|
857
|
+
if (docker.mountCredentials) {
|
|
858
|
+
const claudeArtifacts = resolveDockerClaudeArtifacts(home, docker.containerName, docker.containerWorkdir);
|
|
859
|
+
const credArtifacts = resolveDockerCredentialArtifacts(home);
|
|
860
|
+
extraMounts.push(...claudeArtifacts.mounts, ...credArtifacts.mounts);
|
|
861
|
+
seedCopies = [...claudeArtifacts.seedCopies, ...credArtifacts.seedCopies];
|
|
862
|
+
}
|
|
863
|
+
const envCreate = {
|
|
864
|
+
HOME: CONTAINER_HOME,
|
|
865
|
+
TERM: 'xterm-256color',
|
|
866
|
+
COLORTERM: 'truecolor',
|
|
867
|
+
// Force a UTF-8 locale (the base image defaults to POSIX/C). Without this, tmux
|
|
868
|
+
// runs in non-UTF-8 mode and renders Claude's Unicode box-drawing (─│┌┐) as raw
|
|
869
|
+
// VT100 ACS glyphs (`qqqq…`). `C.UTF-8` is built into glibc (no locale-gen).
|
|
870
|
+
LANG: 'C.UTF-8',
|
|
871
|
+
LC_ALL: 'C.UTF-8',
|
|
872
|
+
// Give claude a temp dir it will own inside HOME. Its default `/tmp/claude-<uid>`
|
|
873
|
+
// is refused when that path pre-exists root-owned — which happens when the
|
|
874
|
+
// workspace bind-mount path traverses it (e.g. a workspace under /tmp/claude-<uid>).
|
|
875
|
+
// A nonexistent HOME subpath is created+owned by the running uid, so this is robust
|
|
876
|
+
// to any workspace location. Non-secret path, safe to be committed on export.
|
|
877
|
+
CLAUDE_CODE_TMPDIR: `${CONTAINER_HOME}/.cache/codeman-claude-tmp`,
|
|
878
|
+
};
|
|
879
|
+
if (docker.hooksEnabled) {
|
|
880
|
+
// Derive a container-reachable API url (scheme + port preserved; host swapped
|
|
881
|
+
// for the engine gateway alias). Prod is HTTPS on 3000.
|
|
882
|
+
envCreate.CODEMAN_API_URL = containerApiUrl(process.env.CODEMAN_API_URL, docker.engine);
|
|
883
|
+
const hookSecretPath = dataPath('hook-secret');
|
|
884
|
+
if (existsSync(hookSecretPath)) {
|
|
885
|
+
const dst = `${CONTAINER_HOME}/.codeman/hook-secret`;
|
|
886
|
+
extraMounts.push({ src: hookSecretPath, dst, readonly: true });
|
|
887
|
+
envCreate.CODEMAN_HOOK_SECRET_FILE = dst; // a path is non-secret; the bytes ride the bind mount
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
const createContext = {
|
|
891
|
+
docker,
|
|
892
|
+
sessionId,
|
|
893
|
+
instance: CODEMAN_INSTANCE,
|
|
894
|
+
userArgs,
|
|
895
|
+
credentialMounts,
|
|
896
|
+
extraMounts,
|
|
897
|
+
envCreate,
|
|
898
|
+
addHostGateway: !isDesktop,
|
|
899
|
+
gatewayAlias,
|
|
900
|
+
};
|
|
901
|
+
const execEnv = {
|
|
902
|
+
TERM: 'xterm-256color',
|
|
903
|
+
COLORTERM: 'truecolor',
|
|
904
|
+
// UTF-8 at exec time too, so the tmux CLIENT this exec launches is UTF-8 and
|
|
905
|
+
// renders box-drawing correctly even when reattaching to a container created
|
|
906
|
+
// before this fix (client_utf8 is per-client, resolved from the exec's locale).
|
|
907
|
+
LANG: 'C.UTF-8',
|
|
908
|
+
LC_ALL: 'C.UTF-8',
|
|
909
|
+
CODEMAN_SESSION_ID: sessionId.slice(0, 8),
|
|
910
|
+
CODEMAN_MUX: '1',
|
|
911
|
+
};
|
|
912
|
+
// NAME-ONLY exec env forwarded from Codeman's process env (the docker client
|
|
913
|
+
// inherits it), so API-key CLIs get their key without it appearing in argv.
|
|
914
|
+
const execEnvNames = mode === 'codex'
|
|
915
|
+
? ['OPENAI_API_KEY', 'CODEX_API_KEY']
|
|
916
|
+
: mode === 'gemini'
|
|
917
|
+
? ['GEMINI_API_KEY', 'GOOGLE_API_KEY']
|
|
918
|
+
: [];
|
|
919
|
+
return { mode, docker, sessionId, resumeSessionId, createContext, execEnv, execEnvNames, seedCopies };
|
|
920
|
+
}
|
|
703
921
|
/**
|
|
704
922
|
* Set sensitive environment variables on a tmux session via setenv.
|
|
705
923
|
* These are inherited by panes but not visible in ps output or tmux history.
|
|
@@ -1075,7 +1293,7 @@ export class TmuxManager extends EventEmitter {
|
|
|
1075
1293
|
* In test mode: creates an in-memory session only (no real tmux session).
|
|
1076
1294
|
*/
|
|
1077
1295
|
async createSession(options) {
|
|
1078
|
-
const { sessionId, workingDir, mode, name, niceConfig, model, claudeMode, allowedTools, openCodeConfig, codexConfig, geminiConfig, resumeSessionId, envOverrides, effort, historyLimit = DEFAULT_TMUX_HISTORY_LIMIT, remote, } = options;
|
|
1296
|
+
const { sessionId, workingDir, mode, name, niceConfig, model, claudeMode, allowedTools, openCodeConfig, codexConfig, geminiConfig, resumeSessionId, envOverrides, effort, historyLimit = DEFAULT_TMUX_HISTORY_LIMIT, remote, docker, } = options;
|
|
1079
1297
|
const muxName = `codeman-${sessionId.slice(0, 8)}`;
|
|
1080
1298
|
if (!isValidMuxName(muxName)) {
|
|
1081
1299
|
throw new Error('Invalid session name: contains unsafe characters');
|
|
@@ -1092,6 +1310,7 @@ export class TmuxManager extends EventEmitter {
|
|
|
1092
1310
|
createdAt: Date.now(),
|
|
1093
1311
|
workingDir,
|
|
1094
1312
|
remote,
|
|
1313
|
+
docker,
|
|
1095
1314
|
mode,
|
|
1096
1315
|
attached: false,
|
|
1097
1316
|
name,
|
|
@@ -1132,7 +1351,11 @@ export class TmuxManager extends EventEmitter {
|
|
|
1132
1351
|
try {
|
|
1133
1352
|
// Build the full command to run inside tmux
|
|
1134
1353
|
const localFullCmd = `${buildNofileLimitCommand()} && ${pathExport}${envExportsStr} && ${cmd}`;
|
|
1135
|
-
const fullCmd =
|
|
1354
|
+
const fullCmd = docker
|
|
1355
|
+
? buildDockerLaunchCommand(resolveDockerLaunchOptions(mode, docker, sessionId, resumeSessionId))
|
|
1356
|
+
: remote
|
|
1357
|
+
? buildRemoteLaunchCommand({ mode, remote, sessionId })
|
|
1358
|
+
: localFullCmd;
|
|
1136
1359
|
// Create tmux session in three steps to handle cold-start (no server running)
|
|
1137
1360
|
// and avoid the race where the command exits before remain-on-exit is set:
|
|
1138
1361
|
// 1. Create session with default shell (starts tmux server, stays alive)
|
|
@@ -1180,7 +1403,7 @@ export class TmuxManager extends EventEmitter {
|
|
|
1180
1403
|
this.applyEnvOverrides(muxName, envOverrides);
|
|
1181
1404
|
// Replace the shell with the actual command (no echo in terminal). Keep
|
|
1182
1405
|
// pane launch in /tmp, then cd inside bash against the current mount table.
|
|
1183
|
-
const launchCmd = remote ? fullCmd : `cd ${JSON.stringify(workingDir)} && ${fullCmd}`;
|
|
1406
|
+
const launchCmd = remote || docker ? fullCmd : `cd ${JSON.stringify(workingDir)} && ${fullCmd}`;
|
|
1184
1407
|
execSync(`${this.tmux()} respawn-pane -k -c ${TMUX_LAUNCH_CWD} -t "${muxName}" bash -c ${JSON.stringify(launchCmd)}`, {
|
|
1185
1408
|
timeout: EXEC_TIMEOUT_MS,
|
|
1186
1409
|
stdio: 'ignore',
|
|
@@ -1244,6 +1467,7 @@ export class TmuxManager extends EventEmitter {
|
|
|
1244
1467
|
createdAt: Date.now(),
|
|
1245
1468
|
workingDir,
|
|
1246
1469
|
remote,
|
|
1470
|
+
docker,
|
|
1247
1471
|
mode,
|
|
1248
1472
|
attached: false,
|
|
1249
1473
|
name,
|
|
@@ -1311,7 +1535,7 @@ export class TmuxManager extends EventEmitter {
|
|
|
1311
1535
|
* preserving the session and its scrollback buffer.
|
|
1312
1536
|
*/
|
|
1313
1537
|
async respawnPane(options) {
|
|
1314
|
-
const { sessionId, workingDir, mode, niceConfig, model, claudeMode, allowedTools, openCodeConfig, codexConfig, geminiConfig, resumeSessionId, envOverrides, effort, historyLimit = DEFAULT_TMUX_HISTORY_LIMIT, remote, } = options;
|
|
1538
|
+
const { sessionId, workingDir, mode, niceConfig, model, claudeMode, allowedTools, openCodeConfig, codexConfig, geminiConfig, resumeSessionId, envOverrides, effort, historyLimit = DEFAULT_TMUX_HISTORY_LIMIT, remote, docker, } = options;
|
|
1315
1539
|
const session = this.sessions.get(sessionId);
|
|
1316
1540
|
if (!session)
|
|
1317
1541
|
return null;
|
|
@@ -1345,7 +1569,11 @@ export class TmuxManager extends EventEmitter {
|
|
|
1345
1569
|
const config = niceConfig || DEFAULT_NICE_CONFIG;
|
|
1346
1570
|
const cmd = wrapWithNice(baseCmd, config);
|
|
1347
1571
|
const localFullCmd = `${buildNofileLimitCommand()} && ${pathExport}${envExportsStr} && ${cmd}`;
|
|
1348
|
-
const fullCmd =
|
|
1572
|
+
const fullCmd = docker
|
|
1573
|
+
? buildDockerLaunchCommand(resolveDockerLaunchOptions(mode, docker, sessionId, resumeSessionId))
|
|
1574
|
+
: remote
|
|
1575
|
+
? buildRemoteLaunchCommand({ mode, remote, sessionId })
|
|
1576
|
+
: localFullCmd;
|
|
1349
1577
|
try {
|
|
1350
1578
|
// For OpenCode: set sensitive env vars via tmux setenv before respawn
|
|
1351
1579
|
if (mode === 'opencode') {
|
|
@@ -1361,7 +1589,7 @@ export class TmuxManager extends EventEmitter {
|
|
|
1361
1589
|
// Re-apply user env overrides before respawn so the new shell inherits them.
|
|
1362
1590
|
this.applyEnvOverrides(muxName, envOverrides);
|
|
1363
1591
|
// -c /tmp + cd bounce — see createSession() for rationale (stale FUSE state).
|
|
1364
|
-
const launchCmd = remote ? fullCmd : `cd ${JSON.stringify(workingDir)} && ${fullCmd}`;
|
|
1592
|
+
const launchCmd = remote || docker ? fullCmd : `cd ${JSON.stringify(workingDir)} && ${fullCmd}`;
|
|
1365
1593
|
await execAsync(`${this.tmux()} respawn-pane -k -c ${TMUX_LAUNCH_CWD} -t "${muxName}" bash -c ${JSON.stringify(launchCmd)}`, {
|
|
1366
1594
|
timeout: EXEC_TIMEOUT_MS,
|
|
1367
1595
|
});
|
|
@@ -1535,6 +1763,18 @@ export class TmuxManager extends EventEmitter {
|
|
|
1535
1763
|
// Best-effort — a failure here must not affect the local kill result.
|
|
1536
1764
|
}
|
|
1537
1765
|
}
|
|
1766
|
+
// Strategy 3c: Docker sessions run a DURABLE in-container tmux session. Kill
|
|
1767
|
+
// ONLY this session's in-container tmux session (best-effort). The container is
|
|
1768
|
+
// PER-CASE and shared by the case's other sessions, so we deliberately do NOT
|
|
1769
|
+
// `docker stop` it here — stopping/removing is an explicit teardown/case-delete.
|
|
1770
|
+
if (session.docker && !IS_TEST_MODE) {
|
|
1771
|
+
try {
|
|
1772
|
+
exec(buildDockerKillCommand({ docker: session.docker, sessionId }), { timeout: EXEC_TIMEOUT_MS }, () => { });
|
|
1773
|
+
}
|
|
1774
|
+
catch {
|
|
1775
|
+
// Best-effort — never affects the local kill result.
|
|
1776
|
+
}
|
|
1777
|
+
}
|
|
1538
1778
|
// Strategy 4: Direct kill by PID as final fallback
|
|
1539
1779
|
if (this.isProcessAlive(currentPid)) {
|
|
1540
1780
|
try {
|