@kothar-hq/host-container-capabilities 0.9.1 → 0.10.0
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
CHANGED
|
@@ -5,7 +5,7 @@ An [agent skill](https://agentskills.io) that finds out which container engines
|
|
|
5
5
|
Coding agents run on Windows hosts (some with WSL), on Linux and macOS, inside WSL, and in cloud sandboxes. An agent that assumes `docker` reaches a Linux engine gets it wrong on a Windows host in Windows-container mode, and one that "fixes" that by switching Docker Desktop stops every container on the host. This skill gives the agent a probed report instead:
|
|
6
6
|
|
|
7
7
|
- **Windows engine:** whether one answers, and the isolation (`process` or `hyperv`) that ltsc2019, ltsc2022 and ltsc2025 images need on this host build.
|
|
8
|
-
- **Linux engine:** the default WSL distro's own Docker, then other WSL 2 distros, then the default docker context, then an existing Docker Desktop Linux pipe. On Linux and macOS, the docker CLI's current context.
|
|
8
|
+
- **Linux engine:** the default WSL distro's own Docker, then other WSL 2 distros, then the default docker context, then an existing Docker Desktop Linux pipe, then Podman. On Linux and macOS, the docker CLI's current context, then Podman. The report says whether the engine is rootless and whether SELinux is enforced.
|
|
9
9
|
- **Routing:** run locally when the engine exists, otherwise offer the project's CI workflow.
|
|
10
10
|
- **Shared-host safety:** everything the agent creates is named and labelled, and only that is cleaned up. Docker Desktop is never switched, and no daemon is started or installed without asking.
|
|
11
11
|
|
|
@@ -54,7 +54,7 @@ sh .claude/skills/host-container-capabilities/scripts/host-capabilities.sh
|
|
|
54
54
|
|
|
55
55
|
- Windows: PowerShell 7. Linux, macOS and WSL: a POSIX `sh`.
|
|
56
56
|
- The installer needs Node.js 20 or later. The skill itself doesn't need Node.
|
|
57
|
-
- Docker
|
|
57
|
+
- Docker and Podman are probed today. Other runtimes are tracked in the [issues](https://github.com/kothar-hq/host-container-capabilities/issues).
|
|
58
58
|
|
|
59
59
|
## Versions
|
|
60
60
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kothar-hq/host-container-capabilities",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "An agent skill that finds out which container engines a host has (Windows, Linux, WSL) and routes container work to a local engine or to CI.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agent-skill",
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: host-container-capabilities
|
|
3
|
-
description: Find out which container engines this host has (a Windows engine for Windows containers, a Linux engine such as a WSL distro's own Docker, both, or neither) and route container work to a local engine or to CI. Use before any docker command, before building or testing an image, before running Linux tools such as shellcheck or actionlint in a container, or when asked whether this machine can run Linux or Windows containers.
|
|
3
|
+
description: Find out which container engines this host has (a Windows engine for Windows containers, a Linux engine such as Docker, Podman or a WSL distro's own Docker, both, or neither) and route container work to a local engine or to CI. Use before any docker or podman command, before building or testing an image, before running Linux tools such as shellcheck or actionlint in a container, or when asked whether this machine can run Linux or Windows containers.
|
|
4
4
|
license: MIT
|
|
5
5
|
metadata:
|
|
6
|
-
version: "0.
|
|
6
|
+
version: "0.10.0"
|
|
7
7
|
repository: https://github.com/kothar-hq/host-container-capabilities
|
|
8
8
|
---
|
|
9
9
|
|
|
@@ -31,10 +31,12 @@ The fields that decide things:
|
|
|
31
31
|
| `platform` | `windows`, `wsl` (the agent is running inside WSL), `linux` or `macos` |
|
|
32
32
|
| `windowsEngine.available`, `.invoke` | Whether a Windows engine answers, and the command that reaches it (`docker`, or `docker.exe` from inside WSL) |
|
|
33
33
|
| `windowsEngine.ltsc.<2019\|2022\|2025>.isolation` | `process`, `hyperv`, or `null` when that Windows version's images can't run here. Only filled in on Windows. |
|
|
34
|
-
| `linuxEngine.available`, `.invoke` | Whether a Linux engine answers, and the command that reaches it, for example `["wsl.exe","--distribution","Ubuntu","--exec","docker"]` |
|
|
34
|
+
| `linuxEngine.available`, `.invoke` | Whether a Linux engine answers, and the command that reaches it, for example `["wsl.exe","--distribution","Ubuntu","--exec","docker"]` or `["podman"]` |
|
|
35
|
+
| `linuxEngine.kind` | `docker`, `wsl` (Docker inside a WSL distro), `docker-desktop-linux` or `podman`. It decides the flags in section 3. |
|
|
36
|
+
| `linuxEngine.rootless`, `.selinux` | Whether the engine runs rootless, and whether SELinux is enforced on its host. They decide the user and mount flags in section 3. |
|
|
35
37
|
| `linuxEngine.repoPath` | The project's path as the Linux engine's host sees it, for bind mounts |
|
|
36
38
|
| `linuxEngine.containers` minus `.claudeContainers` | Containers that belong to someone else (the user's devcontainers, CI runners). Leave them alone. |
|
|
37
|
-
| `linuxEngine.reason`, `.tried` | Why there's no Linux engine, and what was tried |
|
|
39
|
+
| `linuxEngine.reason`, `.tried` | Why there's no Linux engine (for example `socket permission denied`), and what was tried |
|
|
38
40
|
| `policy.sharedHost` | `true` unless the user overrode it. Shared means strict cleanup (section 4). |
|
|
39
41
|
| `policy.createdByLabel`, `.sessionLabelKey` | The labels to put on everything you create (section 4) |
|
|
40
42
|
| `github.authenticated` | Whether `gh` can dispatch workflows |
|
|
@@ -65,6 +67,17 @@ $linux = $report.linuxEngine
|
|
|
65
67
|
--volume "$($linux.repoPath):/src:ro" --workdir /src rhysd/actionlint:1.7.12 -color
|
|
66
68
|
```
|
|
67
69
|
|
|
70
|
+
- **Podman takes the same arguments** as docker for everything this skill does (`run`, `build`, `ps`, `images`, `rm`, `rmi`, `--label`, `--filter label=`). Give images their full name (`docker.io/rhysd/actionlint:1.7.12`): Podman may not search Docker Hub for a short name.
|
|
71
|
+
- **Files written to a bind mount should belong to the user.** Which flag does that depends on the engine:
|
|
72
|
+
|
|
73
|
+
| `kind` | `rootless` | Flag |
|
|
74
|
+
| --- | --- | --- |
|
|
75
|
+
| `podman` | `true` | `--userns=keep-id` |
|
|
76
|
+
| `podman` | `false` | `--user "$(id -u):$(id -g)"` |
|
|
77
|
+
| any Docker kind | `false` | `--user "$(id -u):$(id -g)"` |
|
|
78
|
+
| any Docker kind | `true` | none: root in the container is already the user on the host. `--user` would map to a different host user. |
|
|
79
|
+
|
|
80
|
+
- **SELinux:** when `linuxEngine.selinux` is `true`, add `:Z` to a bind mount (`--volume "$PWD:/src:Z"`, or `:ro,Z`), or the container can't read it. `:Z` relabels the directory, so only use it on the project, never on a home directory or a system path.
|
|
68
81
|
- **Prefer PowerShell on Windows.** Git Bash rewrites arguments that look like paths (`/src` becomes `C:/Program Files/Git/src`), so in Bash prefix the command with `MSYS_NO_PATHCONV=1`.
|
|
69
82
|
- **`--exec` runs docker without a shell** inside the distro. Don't pass `bash -lc '...'` through `wsl.exe`: the quoting breaks and `$variables` are expanded in the wrong place.
|
|
70
83
|
- **`/mnt/c` is slow.** That's fine for linters and for reading the project. For a real build, copy the sources into the container or a Linux directory instead of building on the bind mount.
|
|
@@ -80,7 +93,7 @@ On a shared host (`policy.sharedHost`, the default), everything you create carri
|
|
|
80
93
|
|
|
81
94
|
Prefer images that are already present (`docker images`). If you pull one, note that it wasn't there before, and remove exactly that reference when you're done.
|
|
82
95
|
|
|
83
|
-
Clean up only what carries your session's label, on the engine you created it on:
|
|
96
|
+
Clean up only what carries your session's label, on the engine you created it on. Put the engine's `invoke` in place of `docker`. The same commands work with Podman:
|
|
84
97
|
|
|
85
98
|
```sh
|
|
86
99
|
docker ps -aq --filter label=<policy.sessionLabelKey>=<tag> | xargs -r docker rm -f
|
|
@@ -91,9 +104,12 @@ Never `docker system prune`, `docker image prune` or `docker builder prune` on a
|
|
|
91
104
|
|
|
92
105
|
## 5. When an engine is missing
|
|
93
106
|
|
|
107
|
+
- **`linuxEngine.reason` is `socket permission denied`:** Docker is installed and running, but this user can't use its socket. Offer, after asking, to add the user to the `docker` group (`sudo usermod -aG docker "$USER"`, then a new login; this makes the user root-equivalent on the host, so say so), or to set up rootless Docker or Podman instead. Don't work around it with `sudo docker`.
|
|
108
|
+
- **`linuxEngine.reason` is `no docker or podman CLI is installed`** (a cloud sandbox, a fresh machine): offer the CI workflow, or report what's missing.
|
|
94
109
|
- **No Linux engine, but WSL is installed** (`linuxEngine.tried` names the distros): offer to install Docker Engine in the distro, or to start its daemon if `tried` says it isn't reachable. Both are host changes, so ask first. Otherwise, offer the CI workflow.
|
|
95
110
|
- **No Linux engine and no WSL** (Windows without WSL, or a Linux or macOS host without Docker): offer the CI workflow, or report what's missing.
|
|
96
|
-
- **No Windows engine** (Linux, macOS, or a cloud sandbox): Windows container images can only be built in CI, on a Windows runner. Don't try workarounds.
|
|
111
|
+
- **No Windows engine** (Linux, macOS, or a cloud sandbox): Windows container images can only be built in CI, on a Windows runner. Don't try workarounds. Podman can't run Windows containers, so it never fills in `windowsEngine`.
|
|
112
|
+
- **A remote Windows engine:** when the docker CLI's current context (or `DOCKER_HOST`) reaches a Windows engine from Linux or macOS, `windowsEngine.invoke` is `["docker"]`. Bind mounts of this host don't work there, and `ltsc` is `null`.
|
|
97
113
|
|
|
98
114
|
## 6. Never
|
|
99
115
|
|
|
@@ -117,7 +133,7 @@ Per host, in `.claude/host-container-capabilities.overrides.local.json` in the p
|
|
|
117
133
|
}
|
|
118
134
|
```
|
|
119
135
|
|
|
120
|
-
- `linuxEngine`: `auto` (the default), `none`, `wsl:<distro>` (Windows only), `docker` (the default docker context, if it's a Linux engine), or `docker-desktop-linux` (Windows only: Docker Desktop's Linux engine pipe, if it already exists; it's never switched to)
|
|
136
|
+
- `linuxEngine`: `auto` (the default: Docker first, then Podman), `none`, `wsl:<distro>` (Windows only: Docker, then Podman, in that distro), `docker` (the default docker context, if it's a Linux engine), `podman` (the podman CLI; where `docker` is the podman-docker shim, the report says `podman`), or `docker-desktop-linux` (Windows only: Docker Desktop's Linux engine pipe, if it already exists; it's never switched to)
|
|
121
137
|
- `sharedHost`: set `false` only on a host that nothing else uses
|
|
122
138
|
- `maxAgeHours`: how long a report is reused
|
|
123
139
|
- `createdByLabel`, `sessionLabelKey`: the labels from section 4, for a project that already has a labelling convention
|
|
@@ -11,8 +11,10 @@ On Windows it checks:
|
|
|
11
11
|
- the Windows engine behind the default docker context: its version, default isolation, and the isolation each of
|
|
12
12
|
ltsc2019, ltsc2022 and ltsc2025 needs on this host build
|
|
13
13
|
- a Linux engine, in this order: the default WSL distro's own Docker, any other WSL 2 distro's Docker (the
|
|
14
|
-
docker-desktop distros are skipped), the default docker context if it is a Linux engine,
|
|
15
|
-
Linux engine pipe if it already exists
|
|
14
|
+
docker-desktop distros are skipped), the default docker context if it is a Linux engine, Docker Desktop's
|
|
15
|
+
Linux engine pipe if it already exists, then Podman in each WSL 2 distro (which includes Podman Desktop's
|
|
16
|
+
podman-machine distros) and the native podman CLI. It stops at the first that answers. Probing a stopped WSL
|
|
17
|
+
distro boots it.
|
|
16
18
|
- gh, and whether it is logged in (the exit code only; nothing it prints is kept)
|
|
17
19
|
|
|
18
20
|
It never switches Docker Desktop between Windows and Linux containers, never starts a daemon, and installs nothing.
|
|
@@ -23,8 +25,8 @@ current directory, otherwise the current directory.
|
|
|
23
25
|
The report is cached in <root>/.claude/host-container-capabilities.windows.local.json, which should be gitignored.
|
|
24
26
|
A cached report is reused until it is older than maxAgeHours, the overrides file is newer than it, or -Refresh is
|
|
25
27
|
given. Overrides are read from <root>/.claude/host-container-capabilities.overrides.local.json, a flat JSON object:
|
|
26
|
-
linuxEngine 'auto' (default), 'none', 'wsl:<distro>',
|
|
27
|
-
'docker-desktop-linux'
|
|
28
|
+
linuxEngine 'auto' (default), 'none', 'wsl:<distro>' (Docker, then Podman, in that distro), 'docker'
|
|
29
|
+
(the default docker context), 'docker-desktop-linux' or 'podman' (the native podman CLI)
|
|
28
30
|
sharedHost true (default). Set false only on a host that nothing else uses.
|
|
29
31
|
maxAgeHours 24 (default)
|
|
30
32
|
createdByLabel the key=value label the agent puts on everything it creates
|
|
@@ -97,8 +99,8 @@ if (Test-Path $overridesPath) {
|
|
|
97
99
|
foreach ($key in @($userOverrides.Keys)) {
|
|
98
100
|
if ($overrides.Contains($key)) { $overrides[$key] = $userOverrides[$key] } else { Write-Warning "Ignoring unknown override '$key'" }
|
|
99
101
|
}
|
|
100
|
-
if ($overrides.linuxEngine -notmatch '^(auto|none|docker|docker-desktop-linux|wsl:.+)$') {
|
|
101
|
-
throw "Override linuxEngine '$($overrides.linuxEngine)' must be auto, none, docker, docker-desktop-linux or wsl:<distro>"
|
|
102
|
+
if ($overrides.linuxEngine -notmatch '^(auto|none|docker|docker-desktop-linux|podman|wsl:.+)$') {
|
|
103
|
+
throw "Override linuxEngine '$($overrides.linuxEngine)' must be auto, none, docker, docker-desktop-linux, podman or wsl:<distro>"
|
|
102
104
|
}
|
|
103
105
|
if ($overrides.createdByLabel -notmatch '^[^=\s]+=\S+$') {
|
|
104
106
|
throw "Override createdByLabel '$($overrides.createdByLabel)' must be key=value, without spaces"
|
|
@@ -136,6 +138,11 @@ function Invoke-Native {
|
|
|
136
138
|
# and sets $script:probeError when the engine doesn't answer.
|
|
137
139
|
function Get-EngineFact {
|
|
138
140
|
param([Parameter(Mandatory)][string[]]$Docker)
|
|
141
|
+
$cli = Invoke-Native ($Docker + @('--version'))
|
|
142
|
+
if ($cli.ExitCode -eq 0 -and $cli.Output -match 'podman') {
|
|
143
|
+
$script:probeError = 'docker is the podman-docker shim'
|
|
144
|
+
return $null
|
|
145
|
+
}
|
|
139
146
|
$version = Invoke-Native ($Docker + @('version', '--format', '{{.Server.Os}}|{{.Server.Version}}'))
|
|
140
147
|
if ($version.ExitCode -ne 0 -or $version.Output -notmatch '^(\w+)\|(\S+)$') {
|
|
141
148
|
$script:probeError = ($version.Output -split "`n" | Where-Object { $_ } | Select-Object -First 1) ?? 'no output'
|
|
@@ -144,6 +151,7 @@ function Get-EngineFact {
|
|
|
144
151
|
$os, $serverVersion = $Matches[1], $Matches[2]
|
|
145
152
|
$info = Invoke-Native ($Docker + @('info', '--format', '{{.OperatingSystem}}|{{.Isolation}}'))
|
|
146
153
|
$operatingSystem, $isolation = if ($info.ExitCode -eq 0) { $info.Output -split '\|', 2 } else { $null, $null }
|
|
154
|
+
$security = Invoke-Native ($Docker + @('info', '--format', '{{.SecurityOptions}}'))
|
|
147
155
|
$endpoint = Invoke-Native ($Docker + @('context', 'inspect', '--format', '{{.Endpoints.docker.Host}}'))
|
|
148
156
|
$names = Invoke-Native ($Docker + @('ps', '-a', '--format', '{{.Names}}'))
|
|
149
157
|
$mine = Invoke-Native ($Docker + @('ps', '-aq', '--filter', "label=$($overrides.createdByLabel)"))
|
|
@@ -152,12 +160,42 @@ function Get-EngineFact {
|
|
|
152
160
|
ServerVersion = $serverVersion
|
|
153
161
|
OperatingSystem = $operatingSystem
|
|
154
162
|
Isolation = if ($isolation) { $isolation } else { $null }
|
|
163
|
+
Rootless = if ($security.ExitCode -eq 0) { $security.Output -match 'rootless' } else { $null }
|
|
164
|
+
Selinux = if ($security.ExitCode -eq 0) { $security.Output -match 'selinux' } else { $null }
|
|
155
165
|
Endpoint = if ($endpoint.ExitCode -eq 0) { $endpoint.Output } else { $null }
|
|
156
166
|
Names = @($names.Output -split "`n" | Where-Object { $_ })
|
|
157
167
|
ClaudeCount = @($mine.Output -split "`n" | Where-Object { $_ }).Count
|
|
158
168
|
}
|
|
159
169
|
}
|
|
160
170
|
|
|
171
|
+
# Asks a Podman CLI (an array such as 'wsl.exe','-d','Fedora','--exec','podman') about its engine, with Podman's
|
|
172
|
+
# own field names. Returns $null and sets $script:probeError when the engine doesn't answer.
|
|
173
|
+
function Get-PodmanFact {
|
|
174
|
+
param([Parameter(Mandatory)][string[]]$Podman)
|
|
175
|
+
$format = '{{.Host.OS}}|{{.Version.Version}}|{{.Host.Distribution.Distribution}} {{.Host.Distribution.Version}}|' +
|
|
176
|
+
'{{.Host.Security.Rootless}}|{{.Host.Security.SELinuxEnabled}}|{{.Host.RemoteSocket.Path}}'
|
|
177
|
+
$info = Invoke-Native ($Podman + @('info', '--format', $format))
|
|
178
|
+
$line = $info.Output -split "`n" | Where-Object { $_ -match '^\w+\|' } | Select-Object -First 1
|
|
179
|
+
if ($info.ExitCode -ne 0 -or -not $line) {
|
|
180
|
+
$script:probeError = ($info.Output -split "`n" | Where-Object { $_ } | Select-Object -First 1) ?? 'no output'
|
|
181
|
+
return $null
|
|
182
|
+
}
|
|
183
|
+
$os, $serverVersion, $operatingSystem, $rootless, $selinux, $endpoint = $line.Trim() -split '\|', 6
|
|
184
|
+
$names = Invoke-Native ($Podman + @('ps', '-a', '--format', '{{.Names}}'))
|
|
185
|
+
$mine = Invoke-Native ($Podman + @('ps', '-aq', '--filter', "label=$($overrides.createdByLabel)"))
|
|
186
|
+
[pscustomobject]@{
|
|
187
|
+
Os = $os
|
|
188
|
+
ServerVersion = $serverVersion
|
|
189
|
+
OperatingSystem = $operatingSystem
|
|
190
|
+
Isolation = $null
|
|
191
|
+
Rootless = if ($rootless -in 'true', 'false') { $rootless -eq 'true' } else { $null }
|
|
192
|
+
Selinux = if ($selinux -in 'true', 'false') { $selinux -eq 'true' } else { $null }
|
|
193
|
+
Endpoint = if ($endpoint) { $endpoint } else { $null }
|
|
194
|
+
Names = @($names.Output -split "`n" | Where-Object { $_ })
|
|
195
|
+
ClaudeCount = @($mine.Output -split "`n" | Where-Object { $_ }).Count
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
161
199
|
# Host
|
|
162
200
|
$os = Get-CimInstance Win32_OperatingSystem
|
|
163
201
|
$hostBuild = [Environment]::OSVersion.Version.Build
|
|
@@ -221,34 +259,49 @@ try {
|
|
|
221
259
|
$candidates = @()
|
|
222
260
|
$wslCandidates = @($distros | Where-Object { $_.version -eq 2 -and $_.name -notlike 'docker-desktop*' } |
|
|
223
261
|
Sort-Object { -not $_.default })
|
|
262
|
+
# Kind 'wsl' is Docker inside a WSL distro. Kind 'podman' is Podman, inside a WSL distro (Distro is set, which
|
|
263
|
+
# covers Podman Desktop's podman-machine distros) or through the native podman CLI (Distro is $null).
|
|
264
|
+
$nativePodman = [pscustomobject]@{ Kind = 'podman'; Distro = $null }
|
|
224
265
|
switch -Regex ($overrides.linuxEngine) {
|
|
225
266
|
'^auto$' {
|
|
226
267
|
$candidates += $wslCandidates | ForEach-Object { [pscustomobject]@{ Kind = 'wsl'; Distro = $_.name } }
|
|
227
268
|
$candidates += [pscustomobject]@{ Kind = 'docker'; Distro = $null }
|
|
228
269
|
if ($dockerDesktopLinuxPipe) { $candidates += [pscustomobject]@{ Kind = 'docker-desktop-linux'; Distro = $null } }
|
|
270
|
+
$candidates += $wslCandidates | ForEach-Object { [pscustomobject]@{ Kind = 'podman'; Distro = $_.name } }
|
|
271
|
+
if (Get-Command podman -ErrorAction SilentlyContinue) { $candidates += $nativePodman }
|
|
272
|
+
}
|
|
273
|
+
'^wsl:(.+)$' {
|
|
274
|
+
$candidates += [pscustomobject]@{ Kind = 'wsl'; Distro = $Matches[1] }
|
|
275
|
+
$candidates += [pscustomobject]@{ Kind = 'podman'; Distro = $Matches[1] }
|
|
229
276
|
}
|
|
230
|
-
'^
|
|
277
|
+
'^podman$' { $candidates += $nativePodman }
|
|
231
278
|
'^(docker|docker-desktop-linux)$' { $candidates += [pscustomobject]@{ Kind = $Matches[1]; Distro = $null } }
|
|
232
279
|
}
|
|
233
280
|
|
|
234
281
|
$linuxEngine = [ordered]@{
|
|
235
282
|
available = $false; kind = $null; distro = $null; invoke = $null; endpoint = $null; serverVersion = $null
|
|
236
|
-
operatingSystem = $null;
|
|
283
|
+
operatingSystem = $null; rootless = $null; selinux = $null; containers = $null; claudeContainers = $null
|
|
284
|
+
repoPath = $null; reason = $null; tried = @()
|
|
237
285
|
}
|
|
238
286
|
foreach ($candidate in $candidates) {
|
|
239
287
|
$docker = switch ($candidate.Kind) {
|
|
240
288
|
'wsl' { @('wsl.exe', '--distribution', $candidate.Distro, '--exec', 'docker') }
|
|
241
289
|
'docker' { @('docker') }
|
|
242
290
|
'docker-desktop-linux' { @('docker', '--host', 'npipe:////./pipe/dockerDesktopLinuxEngine') }
|
|
291
|
+
'podman' {
|
|
292
|
+
if ($candidate.Distro) { @('wsl.exe', '--distribution', $candidate.Distro, '--exec', 'podman') } else { @('podman') }
|
|
293
|
+
}
|
|
243
294
|
}
|
|
244
|
-
$fact = if ($candidate.Kind -eq 'docker') { $defaultEngine }
|
|
295
|
+
$fact = if ($candidate.Kind -eq 'docker') { $defaultEngine }
|
|
296
|
+
elseif ($candidate.Kind -eq 'podman') { Get-PodmanFact $docker }
|
|
297
|
+
else { Get-EngineFact $docker }
|
|
245
298
|
$failure = if (-not $fact) { if ($candidate.Kind -eq 'docker') { $defaultError } else { $script:probeError } }
|
|
246
299
|
elseif ($fact.Os -ne 'linux') { "it is a $($fact.Os) engine" }
|
|
247
300
|
if ($failure) {
|
|
248
301
|
$linuxEngine.tried += [ordered]@{ kind = $candidate.Kind; distro = $candidate.Distro; error = $failure }
|
|
249
302
|
continue
|
|
250
303
|
}
|
|
251
|
-
$repoPath = if ($candidate.
|
|
304
|
+
$repoPath = if ($candidate.Distro) {
|
|
252
305
|
(Invoke-Native @('wsl.exe', '--distribution', $candidate.Distro, '--exec', 'wslpath', '-a', $projectRoot)).Output
|
|
253
306
|
} else { $null }
|
|
254
307
|
$linuxEngine.available = $true
|
|
@@ -258,6 +311,8 @@ try {
|
|
|
258
311
|
$linuxEngine.endpoint = $fact.Endpoint
|
|
259
312
|
$linuxEngine.serverVersion = $fact.ServerVersion
|
|
260
313
|
$linuxEngine.operatingSystem = $fact.OperatingSystem
|
|
314
|
+
$linuxEngine.rootless = $fact.Rootless
|
|
315
|
+
$linuxEngine.selinux = $fact.Selinux
|
|
261
316
|
$linuxEngine.containers = $fact.Names.Count
|
|
262
317
|
$linuxEngine.claudeContainers = $fact.ClaudeCount
|
|
263
318
|
$linuxEngine.repoPath = $repoPath
|
|
@@ -265,6 +320,7 @@ try {
|
|
|
265
320
|
}
|
|
266
321
|
if (-not $linuxEngine.available) {
|
|
267
322
|
$linuxEngine.reason = if ($overrides.linuxEngine -eq 'none') { 'disabled by the linuxEngine override' }
|
|
323
|
+
elseif ($linuxEngine.tried | Where-Object { $_.error -match 'permission denied' }) { 'socket permission denied' }
|
|
268
324
|
elseif (-not $wsl.installed) { 'WSL is not installed and no other Linux engine answered' }
|
|
269
325
|
else { 'no candidate answered; see tried' }
|
|
270
326
|
}
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
# The project root is $HOST_CONTAINER_CAPABILITIES_ROOT, else the git work tree around the current directory,
|
|
10
10
|
# else the current directory. The report is cached in <root>/.claude/host-container-capabilities.<platform>.local.json.
|
|
11
11
|
# Overrides are read from <root>/.claude/host-container-capabilities.overrides.local.json, a flat JSON object with
|
|
12
|
-
# linuxEngine (auto, none or
|
|
12
|
+
# linuxEngine (auto, none, docker or podman), sharedHost (true), maxAgeHours (24), createdByLabel, sessionLabelKey and
|
|
13
13
|
# runnerContainerPattern. It never starts a daemon and installs nothing.
|
|
14
14
|
set -eu
|
|
15
15
|
|
|
@@ -66,8 +66,8 @@ ov_session=$(json_value "$overrides" sessionLabelKey)
|
|
|
66
66
|
ov_runners=$(json_value "$overrides" runnerContainerPattern)
|
|
67
67
|
: "${ov_engine:=auto}" "${ov_shared:=true}" "${ov_age:=24}"
|
|
68
68
|
: "${ov_label:=io.github.kothar-hq.created-by=agent}" "${ov_session:=io.github.kothar-hq.session}"
|
|
69
|
-
case "$ov_engine" in auto | none | docker) ;; *)
|
|
70
|
-
echo "host-capabilities.sh: override linuxEngine '$ov_engine' must be auto, none or
|
|
69
|
+
case "$ov_engine" in auto | none | docker | podman) ;; *)
|
|
70
|
+
echo "host-capabilities.sh: override linuxEngine '$ov_engine' must be auto, none, docker or podman here" >&2; exit 2 ;;
|
|
71
71
|
esac
|
|
72
72
|
case "$ov_shared" in true | false) ;; *) echo "host-capabilities.sh: override sharedHost must be true or false" >&2; exit 2 ;; esac
|
|
73
73
|
case "$ov_age" in '' | *[!0-9]*) echo "host-capabilities.sh: override maxAgeHours must be a whole number" >&2; exit 2 ;; esac
|
|
@@ -84,46 +84,119 @@ if [ "$refresh" = 0 ] && [ -f "$cache" ] && [ -z "$(find "$overrides" -newer "$c
|
|
|
84
84
|
fi
|
|
85
85
|
fi
|
|
86
86
|
|
|
87
|
-
#
|
|
88
|
-
#
|
|
89
|
-
|
|
90
|
-
p_os='' p_version='' p_osname='' p_isolation='' p_endpoint='' p_total='' p_mine='' p_error=''
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
87
|
+
# The probes set p_os, p_version, p_osname, p_isolation, p_endpoint, p_rootless, p_selinux, p_total and p_mine,
|
|
88
|
+
# or p_error when the engine doesn't answer.
|
|
89
|
+
reset_probe() {
|
|
90
|
+
p_os='' p_version='' p_osname='' p_isolation='' p_endpoint='' p_rootless='' p_selinux='' p_total='' p_mine='' p_error=''
|
|
91
|
+
}
|
|
92
|
+
# first_error <command...>: the first non-empty line the command prints on stderr
|
|
93
|
+
first_error() {
|
|
94
|
+
p_error=$("$@" 2>&1 >/dev/null | sed -n '/./{p;q;}' || true)
|
|
95
|
+
: "${p_error:=no output}"
|
|
96
|
+
}
|
|
97
|
+
count_containers() {
|
|
98
|
+
p_total=$("$@" ps -aq 2>/dev/null | wc -l | tr -d ' ')
|
|
99
|
+
p_mine=$("$@" ps -aq --filter "label=$ov_label" 2>/dev/null | wc -l | tr -d ' ')
|
|
100
|
+
}
|
|
101
|
+
# is_podman <cli>: true when the CLI is Podman, which includes the podman-docker shim named docker
|
|
102
|
+
is_podman() { "$1" --version 2>/dev/null | grep -qi podman; }
|
|
103
|
+
|
|
104
|
+
# probe_docker <docker command...>: asks a docker CLI about its engine
|
|
105
|
+
probe_docker() {
|
|
106
|
+
reset_probe
|
|
107
|
+
if ! out=$("$@" version --format '{{.Server.Os}}|{{.Server.Version}}' 2>/dev/null) || [ "${out#*|}" = "$out" ]; then
|
|
108
|
+
first_error "$@" version --format '{{.Server.Os}}|{{.Server.Version}}'
|
|
94
109
|
return 1
|
|
95
110
|
fi
|
|
96
111
|
p_os=${out%%|*} p_version=${out#*|}
|
|
97
112
|
info=$("$@" info --format '{{.OperatingSystem}}|{{.Isolation}}' 2>/dev/null || true)
|
|
98
113
|
p_osname=${info%%|*} p_isolation=${info#*|}
|
|
114
|
+
security=$("$@" info --format '{{.SecurityOptions}}' 2>/dev/null || true)
|
|
115
|
+
case "$security" in *rootless*) p_rootless=true ;; *) p_rootless=false ;; esac
|
|
116
|
+
case "$security" in *selinux*) p_selinux=true ;; *) p_selinux=false ;; esac
|
|
99
117
|
p_endpoint=$("$@" context inspect --format '{{.Endpoints.docker.Host}}' 2>/dev/null || true)
|
|
100
|
-
|
|
101
|
-
p_mine=$("$@" ps -aq --filter "label=$ov_label" 2>/dev/null | wc -l | tr -d ' ')
|
|
118
|
+
count_containers "$@"
|
|
102
119
|
}
|
|
103
120
|
|
|
104
|
-
#
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
121
|
+
# probe_podman <podman command...>: asks a Podman CLI about its engine, with Podman's own field names
|
|
122
|
+
probe_podman() {
|
|
123
|
+
reset_probe
|
|
124
|
+
format='{{.Host.OS}}|{{.Version.Version}}|{{.Host.Distribution.Distribution}} {{.Host.Distribution.Version}}|{{.Host.Security.Rootless}}|{{.Host.Security.SELinuxEnabled}}|{{.Host.RemoteSocket.Path}}'
|
|
125
|
+
if ! out=$("$@" info --format "$format" 2>/dev/null) || [ "${out#*|}" = "$out" ]; then
|
|
126
|
+
first_error "$@" info --format "$format"
|
|
127
|
+
return 1
|
|
128
|
+
fi
|
|
129
|
+
IFS='|' read -r p_os p_version p_osname p_rootless p_selinux p_endpoint <<EOF
|
|
130
|
+
$out
|
|
131
|
+
EOF
|
|
132
|
+
case "$p_rootless" in true | false) ;; *) p_rootless='' ;; esac
|
|
133
|
+
case "$p_selinux" in true | false) ;; *) p_selinux='' ;; esac
|
|
134
|
+
case "$p_endpoint" in /*) p_endpoint="unix://$p_endpoint" ;; esac
|
|
135
|
+
count_containers "$@"
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
# Linux engine: Docker first (the docker CLI's current context: DOCKER_HOST, Docker Desktop, colima, a distro's own
|
|
139
|
+
# Docker), then Podman.
|
|
140
|
+
l_available=false l_kind='' l_invoke=null l_version='' l_osname='' l_endpoint='' l_rootless='' l_selinux=''
|
|
141
|
+
l_total='' l_mine='' l_reason='' l_tried='' l_denied=0
|
|
142
|
+
# A Windows engine behind the docker CLI's current context (DOCKER_HOST or a remote context)
|
|
143
|
+
remote_windows=0 rw_version='' rw_endpoint='' rw_isolation=''
|
|
144
|
+
|
|
145
|
+
add_tried() {
|
|
146
|
+
entry="{\"kind\": \"$1\", \"distro\": null, \"error\": $(js "$2")}"
|
|
147
|
+
if [ -n "$l_tried" ]; then l_tried="$l_tried, $entry"; else l_tried=$entry; fi
|
|
148
|
+
case "$2" in *[Pp]ermission\ denied*) l_denied=1 ;; esac
|
|
149
|
+
}
|
|
150
|
+
found_linux() {
|
|
151
|
+
l_available=true l_kind=$1 l_invoke="[\"$2\"]" l_version=$p_version l_osname=$p_osname l_endpoint=$p_endpoint
|
|
152
|
+
l_rootless=$p_rootless l_selinux=$p_selinux l_total=$p_total l_mine=$p_mine
|
|
153
|
+
}
|
|
154
|
+
try_docker() {
|
|
155
|
+
if ! command -v docker >/dev/null 2>&1; then add_tried docker 'docker CLI not installed'; return 1; fi
|
|
156
|
+
if is_podman docker; then add_tried docker 'docker is the podman-docker shim'; return 1; fi
|
|
157
|
+
if ! probe_docker docker; then add_tried docker "$p_error"; return 1; fi
|
|
158
|
+
if [ "$p_os" = linux ]; then found_linux docker docker; return 0; fi
|
|
159
|
+
if [ "$p_os" = windows ]; then
|
|
160
|
+
remote_windows=1 rw_version=$p_version rw_endpoint=$p_endpoint rw_isolation=$p_isolation
|
|
161
|
+
fi
|
|
162
|
+
add_tried docker "it is a $p_os engine"
|
|
163
|
+
return 1
|
|
164
|
+
}
|
|
165
|
+
try_podman() {
|
|
166
|
+
cli=podman
|
|
167
|
+
if ! command -v podman >/dev/null 2>&1; then
|
|
168
|
+
if command -v docker >/dev/null 2>&1 && is_podman docker; then
|
|
169
|
+
cli=docker
|
|
170
|
+
else
|
|
171
|
+
add_tried podman 'podman CLI not installed'
|
|
172
|
+
return 1
|
|
173
|
+
fi
|
|
174
|
+
fi
|
|
175
|
+
if ! probe_podman "$cli"; then add_tried podman "$p_error"; return 1; fi
|
|
176
|
+
if [ "$p_os" = linux ]; then found_linux podman "$cli"; return 0; fi
|
|
177
|
+
add_tried podman "it is a $p_os engine"
|
|
178
|
+
return 1
|
|
179
|
+
}
|
|
180
|
+
case "$ov_engine" in
|
|
181
|
+
none) l_reason='disabled by the linuxEngine override' ;;
|
|
182
|
+
docker) try_docker || true ;;
|
|
183
|
+
podman) try_podman || true ;;
|
|
184
|
+
auto) try_docker || try_podman || true ;;
|
|
185
|
+
esac
|
|
186
|
+
if [ "$l_available" = false ] && [ -z "$l_reason" ]; then
|
|
187
|
+
if [ "$l_denied" = 1 ]; then
|
|
188
|
+
l_reason='socket permission denied'
|
|
189
|
+
elif ! command -v docker >/dev/null 2>&1 && ! command -v podman >/dev/null 2>&1; then
|
|
190
|
+
l_reason='no docker or podman CLI is installed'
|
|
191
|
+
else
|
|
192
|
+
l_reason='no candidate answered; see tried'
|
|
193
|
+
fi
|
|
118
194
|
fi
|
|
119
195
|
|
|
120
|
-
# Windows engine:
|
|
196
|
+
# Windows engine: from WSL, through Windows' docker.exe. From anywhere, a remote one behind the docker CLI's
|
|
197
|
+
# current context. Podman can't run Windows containers.
|
|
121
198
|
w_available=false w_invoke=null w_endpoint='' w_version='' w_isolation='' w_runners='' w_reason=''
|
|
122
|
-
if [ "$platform"
|
|
123
|
-
w_reason="a $platform host has no Windows engine"
|
|
124
|
-
elif ! command -v docker.exe >/dev/null 2>&1; then
|
|
125
|
-
w_reason='docker.exe is not on the PATH (WSL interop off, or no Windows Docker)'
|
|
126
|
-
elif probe docker.exe && [ "$p_os" = windows ]; then
|
|
199
|
+
if [ "$platform" = wsl ] && command -v docker.exe >/dev/null 2>&1 && probe_docker docker.exe && [ "$p_os" = windows ]; then
|
|
127
200
|
w_available=true w_invoke='["docker.exe"]' w_endpoint=$p_endpoint w_version=$p_version w_isolation=$p_isolation
|
|
128
201
|
if [ -n "$ov_runners" ]; then
|
|
129
202
|
w_runners=0
|
|
@@ -133,6 +206,13 @@ elif probe docker.exe && [ "$p_os" = windows ]; then
|
|
|
133
206
|
done
|
|
134
207
|
fi
|
|
135
208
|
w_reason='per-LTSC isolation is only worked out on Windows: run Get-HostCapabilities.ps1 with pwsh.exe'
|
|
209
|
+
elif [ "$remote_windows" = 1 ]; then
|
|
210
|
+
w_available=true w_invoke='["docker"]' w_endpoint=$rw_endpoint w_version=$rw_version w_isolation=$rw_isolation
|
|
211
|
+
w_reason='a remote engine behind the docker context: per-LTSC isolation is not worked out, and bind mounts of this host do not work'
|
|
212
|
+
elif [ "$platform" != wsl ]; then
|
|
213
|
+
w_reason="a $platform host has no Windows engine, and the docker context does not reach one"
|
|
214
|
+
elif ! command -v docker.exe >/dev/null 2>&1; then
|
|
215
|
+
w_reason='docker.exe is not on the PATH (WSL interop off, or no Windows Docker)'
|
|
136
216
|
else
|
|
137
217
|
if [ -n "$p_os" ]; then p_error="docker.exe reaches a $p_os engine"; fi
|
|
138
218
|
w_reason="docker.exe: $p_error"
|
|
@@ -192,6 +272,8 @@ cat >"$cache" <<EOF
|
|
|
192
272
|
"endpoint": $(js "$l_endpoint"),
|
|
193
273
|
"serverVersion": $(js "$l_version"),
|
|
194
274
|
"operatingSystem": $(js "$l_osname"),
|
|
275
|
+
"rootless": $(jn "$l_rootless"),
|
|
276
|
+
"selinux": $(jn "$l_selinux"),
|
|
195
277
|
"containers": $(jn "$l_total"),
|
|
196
278
|
"claudeContainers": $(jn "$l_mine"),
|
|
197
279
|
"repoPath": $(js "$root"),
|