From affe91d7ed9f6037d81b5e12c97ebf8465985bca Mon Sep 17 00:00:00 2001 From: JD Date: Wed, 28 Oct 2026 00:39:13 -0400 Subject: [PATCH 1/2] test(docker): give the Update-Codeman.sh smoke harness a non-root appdata owner Both Update-Codeman.sh and Start-Codeman.sh derive PUID:PGID from the CODEMAN_APPDATA_PATH owner and refuse a root-owned directory. The smoke harness created that directory with mkdirSync, so under a root test runner (CI or agent containers) it was owned 0:0 and the script exited at that guard before its `build --no-cache` and `down`, failing the default and --keep-volumes smoke tests with an empty build/down log. The guard is correct; the harness now chowns appdata to 99:100 (docker/README.md's own example) when running as root, matching a real deployment. --- test/docker-entrypoint.test.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/test/docker-entrypoint.test.ts b/test/docker-entrypoint.test.ts index 417b67fe4..27559e101 100644 --- a/test/docker-entrypoint.test.ts +++ b/test/docker-entrypoint.test.ts @@ -26,7 +26,7 @@ */ import { describe, it, expect, beforeAll, afterAll } from 'vitest'; -import { readFileSync, mkdtempSync, rmSync, writeFileSync, mkdirSync } from 'node:fs'; +import { readFileSync, mkdtempSync, rmSync, writeFileSync, mkdirSync, chownSync } from 'node:fs'; import { execFileSync } from 'node:child_process'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; @@ -309,6 +309,13 @@ describe('Update-Codeman.sh (the scripted major-update path — docker/README.md const casesPath = join(dir, 'cases'); const socketPath = join(dir, 'docker.sock'); // deliberately NOT a real socket — see above mkdirSync(appdataPath); + // Both scripts derive PUID:PGID from the appdata owner and REFUSE a + // root-owned one (correctly: the container must never run as root). + // mkdirSync gives it the runner's uid, so under a root runner (a CI or + // agent container) the update path stopped at that guard before its + // build. Give it the non-root owner docker/README.md's own + // `chown -R 99:100` example sets up, as a real deployment would have. + if (process.getuid?.() === 0) chownSync(appdataPath, 99, 100); mkdirSync(casesPath); writeFileSync(socketPath, ''); From ee97daa4f07ea27f6e68424b9011140a61bf8326 Mon Sep 17 00:00:00 2001 From: JD Date: Wed, 28 Oct 2026 13:33:45 -0400 Subject: [PATCH 2/2] test(docker): leave appdata root-owned when the runner cannot chown it Root without CAP_CHOWN (--cap-drop ALL, as Codeman's own Docker case containers run) or a single-uid user namespace makes chownSync throw, which failed all five smoke tests in harness setup instead of master's two. --- test/docker-entrypoint.test.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/test/docker-entrypoint.test.ts b/test/docker-entrypoint.test.ts index 27559e101..b91436586 100644 --- a/test/docker-entrypoint.test.ts +++ b/test/docker-entrypoint.test.ts @@ -315,7 +315,15 @@ describe('Update-Codeman.sh (the scripted major-update path — docker/README.md // agent container) the update path stopped at that guard before its // build. Give it the non-root owner docker/README.md's own // `chown -R 99:100` example sets up, as a real deployment would have. - if (process.getuid?.() === 0) chownSync(appdataPath, 99, 100); + if (process.getuid?.() === 0) { + try { + chownSync(appdataPath, 99, 100); + } catch { + // No CAP_CHOWN (root with --cap-drop ALL, as Codeman's own Docker + // case containers run) or a single-uid user namespace: leave it + // root-owned and let the scripts' own guard refuse, as before. + } + } mkdirSync(casesPath); writeFileSync(socketPath, '');