fix(cso): Windows finds docker.exe only under known-folder roots and says transport is unsupported (#3028)

On Windows /cso looked for docker only in Git's directory and System32, so
Docker Desktop was never found and the gap read as 'not installed'. It now
resolves Program Files, Program Files (x86) and the Windows directory through
PowerShell's [Environment]::GetFolderPath (not the process environment),
checks each candidate's real path against those roots and rejects reparse
points. lib/cso/docker.ts admits only unix:/// endpoints, so a trusted
docker.exe reports 'native Windows Docker transport is not supported yet;
static assessment only'; one outside the roots is refused as outside the
trusted install locations, not overridable. Both carry an anchor. Native
named-pipe transport is not implemented.
This commit is contained in:
garrytan committed 2026-10-06 20:38:00 +00:00
1 parent ba84b6cc68
commit 2ff3b6a854
4 files changed
+227

No files matched your search

+19
View File
@@ -563,6 +563,25 @@ Visual Studio".
**Fix.** Fix the printed compiler error, then re-run `./setup`.
<a id="cso-windows-docker"></a>
### `Docker found at <path>, but native Windows Docker transport is not supported yet; static assessment only.` / `docker.exe at <path> is outside the trusted install locations (...)`
**Meaning.** On Windows, /cso looks for `docker.exe` only under the install
folders Windows reports for Program Files, Program Files (x86) and the Windows
directory, by its real path, with no symlink or junction on the way. A
user-writable directory is untrusted, because the Docker child carries
registry credentials; that refusal cannot be overridden.
Even a trusted `docker.exe` cannot run /cso's isolated containers yet: /cso
admits only a local Unix Docker socket, and Docker Desktop on Windows speaks
over a named pipe. /cso reports this and runs its static assessment only; no
container or runtime check runs.
**Fix.** For runtime checks, run /cso from Linux or macOS (WSL2 counts as
Linux) with a local Docker socket. On Windows, static assessment is the
supported mode; if the refusal named a user directory, install Docker Desktop
under Program Files.
<a id="conductor-auq-hook-removed"></a>
### `removed the AskUserQuestion preference hook: it breaks Conductor's native AskUserQuestion (#2207). ...`
+2
View File
@@ -15,6 +15,7 @@ import {
import { basename, dirname, join, isAbsolute, delimiter, resolve } from 'node:path';
import { redactFindingSpans } from '../redact-engine';
import { CsoError, MAX_OUTPUT } from './contracts';
import { windowsDockerUnavailable } from './windows-docker';
const SOURCE_RUNTIME = /^bun(?:\.exe)?$/i.test(basename(process.execPath));
const WINDOWS_GIT =
@@ -45,6 +46,7 @@ export function executable(name: string): string {
);
}
}
if (process.platform === 'win32' && name.toLowerCase() === 'docker') windowsDockerUnavailable();
for (const directory of TRUSTED_DIRECTORIES) {
const candidates =
process.platform === 'win32'
+112
View File
@@ -0,0 +1,112 @@
import { spawnSync } from 'node:child_process';
import { lstatSync, realpathSync } from 'node:fs';
import { win32 } from 'node:path';
import { CsoError } from './contracts';
export const WINDOWS_DOCKER_ANCHOR =
'https://github.com/garrytan/gstack/blob/main/docs/troubleshooting.md#cso-windows-docker';
export interface WindowsDockerProbe {
/** Known-folder roots (Program Files, Program Files (x86), Windows), never the process environment. */
knownFolders(): string[];
/** Candidate docker.exe paths: a PATH lookup and the Docker Desktop default under each root. */
candidates(roots: string[]): string[];
realpath(path: string): string;
isReparsePoint(path: string): boolean;
}
function under(path: string, root: string): boolean {
const p = win32.resolve(path).toLowerCase();
const r = win32.resolve(root).toLowerCase().replace(/\\+$/, '');
return p.startsWith(`${r}\\`);
}
/** Every existing component of `path` from its drive root down, the file included. */
function components(path: string): string[] {
const resolved = win32.resolve(path);
const root = win32.parse(resolved).root;
const parts = resolved.slice(root.length).split('\\').filter(Boolean);
return parts.map((_, i) => win32.join(root, ...parts.slice(0, i + 1)));
}
/**
* /cso on Windows: find docker.exe only under the known-folder install roots.
* Each candidate's real path must stay under a root and no component may be a
* reparse point (symlink or junction). A bound docker.exe still cannot carry
* /cso's isolation (lib/cso/docker.ts admits only unix:/// endpoints), so this
* always throws: the "not supported yet" outcome when Docker is trusted, the
* refusal when it is outside the trusted locations, or the not-installed error.
*/
export function windowsDockerUnavailable(probe: WindowsDockerProbe = systemProbe()): never {
let roots: string[] = [];
try {
roots = probe.knownFolders().filter((root) => win32.isAbsolute(root));
} catch {}
const found = [...new Set(probe.candidates(roots))].filter((candidate) => {
try {
probe.realpath(candidate);
return true;
} catch {
return false;
}
});
if (!found.length)
throw new CsoError(
'TOOL_UNAVAILABLE',
'docker is not installed in a trusted system executable directory',
);
for (const candidate of found) {
let real = '';
try {
real = probe.realpath(candidate);
if (components(candidate).some((part) => probe.isReparsePoint(part))) continue;
} catch {
continue;
}
if (win32.resolve(real).toLowerCase() !== win32.resolve(candidate).toLowerCase()) continue;
if (!roots.some((root) => under(real, root))) continue;
throw new CsoError(
'TOOL_UNAVAILABLE',
`Docker found at ${real}, but native Windows Docker transport is not supported yet; static assessment only. ${WINDOWS_DOCKER_ANCHOR}`,
);
}
throw new CsoError(
'TOOL_UNAVAILABLE',
`docker.exe at ${found[0]} is outside the trusted install locations (${roots.join(', ') || 'none resolved'}), ` +
'or reaches them through a symlink or junction. A user-writable directory is untrusted for a child that ' +
'carries registry credentials. Fix: install Docker Desktop under Program Files, or run /cso static-only. ' +
`This is not overridable. ${WINDOWS_DOCKER_ANCHOR}`,
);
}
function systemProbe(): WindowsDockerProbe {
const windows = process.env.SystemRoot || 'C:\\Windows';
return {
knownFolders() {
const r = spawnSync(
win32.join(windows, 'System32', 'WindowsPowerShell', 'v1.0', 'powershell.exe'),
[
'-NoProfile',
'-NonInteractive',
'-Command',
"'ProgramFiles','ProgramFilesX86','Windows' | ForEach-Object { [Environment]::GetFolderPath($_) }",
],
{ encoding: 'utf8', timeout: 10_000, windowsHide: true },
);
if (r.status !== 0) return [];
return r.stdout
.split(/\r?\n/)
.map((line) => line.trim())
.filter(Boolean);
},
candidates(roots) {
const onPath = Bun.which('docker');
return [
...(onPath ? [onPath] : []),
...roots.map((root) => win32.join(root, 'Docker', 'Docker', 'resources', 'bin', 'docker.exe')),
];
},
realpath: (path) => realpathSync.native(path),
isReparsePoint: (path) => lstatSync(path).isSymbolicLink(),
};
}
+94
View File
@@ -0,0 +1,94 @@
/**
* /cso on Windows: docker.exe is discovered only under the known-folder
* install roots (resolved through [Environment]::GetFolderPath, not the
* process environment), by real path, with no symlink or junction on the way.
* lib/cso/docker.ts admits only unix:/// endpoints, so a trusted docker.exe
* yields the "native Windows Docker transport is not supported yet; static
* assessment only" outcome, never a run; anything else is refused.
*/
import { describe, expect, test } from 'bun:test';
import * as fs from 'node:fs';
import * as path from 'node:path';
import { CsoError } from '../lib/cso/contracts';
import { WINDOWS_DOCKER_ANCHOR, windowsDockerUnavailable, type WindowsDockerProbe } from '../lib/cso/windows-docker';
const ROOTS = ['C:\\Program Files', 'C:\\Program Files (x86)', 'C:\\Windows'];
const DESKTOP = 'C:\\Program Files\\Docker\\Docker\\resources\\bin\\docker.exe';
function probe(files: Record<string, string>, opts: { reparse?: string[]; roots?: string[]; onPath?: string } = {}): WindowsDockerProbe {
const real = new Map(Object.entries(files).map(([k, v]) => [k.toLowerCase(), v]));
return {
knownFolders: () => opts.roots ?? ROOTS,
candidates: (roots) => [...(opts.onPath ? [opts.onPath] : []), ...roots.map((r) => `${r}\\Docker\\Docker\\resources\\bin\\docker.exe`)],
realpath: (p) => {
const hit = real.get(p.toLowerCase());
if (!hit) throw Object.assign(new Error('ENOENT'), { code: 'ENOENT' });
return hit;
},
isReparsePoint: (p) => (opts.reparse ?? []).some((r) => r.toLowerCase() === p.toLowerCase()),
};
}
function outcome(p: WindowsDockerProbe): string {
try {
windowsDockerUnavailable(p);
} catch (err) {
expect(err).toBeInstanceOf(CsoError);
return (err as Error).message;
}
throw new Error('windowsDockerUnavailable returned');
}
describe('/cso Windows Docker discovery', () => {
test('Docker Desktop under Program Files: not supported yet, static assessment only, with the anchor', () => {
const msg = outcome(probe({ [DESKTOP]: DESKTOP }));
expect(msg).toBe(`Docker found at ${DESKTOP}, but native Windows Docker transport is not supported yet; static assessment only. ${WINDOWS_DOCKER_ANCHOR}`);
expect(WINDOWS_DOCKER_ANCHOR).toBe('https://github.com/garrytan/gstack/blob/main/docs/troubleshooting.md#cso-windows-docker');
});
test('a docker.exe on PATH in a user-writable directory is refused as outside the trusted install locations', () => {
const user = 'C:\\Users\\dev\\AppData\\Local\\bin\\docker.exe';
const msg = outcome(probe({ [user]: user }, { onPath: user }));
expect(msg).toContain(`docker.exe at ${user} is outside the trusted install locations (${ROOTS.join(', ')})`);
expect(msg).toContain('A user-writable directory is untrusted for a child that carries registry credentials');
expect(msg).toContain('Fix: install Docker Desktop under Program Files, or run /cso static-only');
expect(msg).toContain('This is not overridable');
expect(msg).toContain(WINDOWS_DOCKER_ANCHOR);
});
test('a sibling directory that only shares the prefix is not under Program Files', () => {
const evil = 'C:\\Program Files Evil\\docker.exe';
expect(outcome(probe({ [evil]: evil }, { onPath: evil }))).toContain('is outside the trusted install locations');
});
test('the real path decides: a Program Files path that resolves into a user directory is refused', () => {
const escaped = 'C:\\Users\\dev\\docker.exe';
expect(outcome(probe({ [DESKTOP]: escaped }))).toContain(`docker.exe at ${DESKTOP} is outside the trusted install locations`);
});
test('a junction anywhere on the path is refused even when it resolves in place', () => {
const msg = outcome(probe({ [DESKTOP]: DESKTOP }, { reparse: ['C:\\Program Files\\Docker'] }));
expect(msg).toContain('reaches them through a symlink or junction');
});
test('roots come from the known-folder probe: an unresolved probe trusts nothing', () => {
expect(outcome(probe({ [DESKTOP]: DESKTOP }, { roots: [], onPath: DESKTOP }))).toContain('outside the trusted install locations (none resolved)');
});
test('no docker.exe anywhere keeps the not-installed error', () => {
expect(outcome(probe({}))).toBe('docker is not installed in a trusted system executable directory');
});
test('the system probe asks PowerShell for the known folders, never the environment', () => {
const src = fs.readFileSync(path.join(import.meta.dir, '..', 'lib', 'cso', 'windows-docker.ts'), 'utf8');
expect(src).toContain('[Environment]::GetFolderPath($_)');
expect(src).toContain("'ProgramFiles','ProgramFilesX86','Windows'");
expect(src).not.toMatch(/process\.env\.(ProgramFiles|ProgramW6432|"ProgramFiles)/);
expect(src).toContain('realpathSync.native');
});
test('executable("docker") routes Windows through the discovery', () => {
const src = fs.readFileSync(path.join(import.meta.dir, '..', 'lib', 'cso', 'process.ts'), 'utf8');
expect(src).toContain("if (process.platform === 'win32' && name.toLowerCase() === 'docker') windowsDockerUnavailable();");
});
});