@agentshouse/kit 0.1.0-alpha.35 → 0.1.0-alpha.36
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 +82 -8
- package/bin/connect-linux.sh +2 -0
- package/bin/connect-windows.ps1 +430 -0
- package/bootstrap.json +14 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -12,7 +12,7 @@ The Skill set Kit installs is Matt Pocock's
|
|
|
12
12
|
On Linux `amd64` or `arm64`, run the published, versioned bootstrap once:
|
|
13
13
|
|
|
14
14
|
```sh
|
|
15
|
-
bash <(curl -fsSL https://github.com/agentshouse/kit/releases/download/v0.1.0-alpha.
|
|
15
|
+
bash <(curl -fsSL https://github.com/agentshouse/kit/releases/download/v0.1.0-alpha.36/connect-linux.sh)
|
|
16
16
|
```
|
|
17
17
|
|
|
18
18
|
The bootstrap first establishes Docker Engine from what it observes on the
|
|
@@ -80,12 +80,12 @@ outcome.
|
|
|
80
80
|
|
|
81
81
|
The versioned `bootstrap.json` is the machine-readable command source for this
|
|
82
82
|
release. It is exported from the npm package as `@agentshouse/kit/bootstrap`
|
|
83
|
-
(JSON import) and published byte-for-byte beside `connect-linux.sh`
|
|
84
|
-
GitHub release. Its schema version is `1`, its `version` is the Kit package
|
|
83
|
+
(JSON import) and published byte-for-byte beside `connect-linux.sh` and `connect-windows.ps1`
|
|
84
|
+
in the GitHub release. Its schema version is `1`, its `version` is the Kit package
|
|
85
85
|
version, and `hosts` contains only bootstrap scripts published in that release.
|
|
86
86
|
The package exports only the `./bootstrap` subpath; `./package.json` and source
|
|
87
|
-
subpaths are not exported. For this release,
|
|
88
|
-
schema example, `<version>` stands for the package's version:
|
|
87
|
+
subpaths are not exported. For this release, `hosts.linux` and `hosts.windows`
|
|
88
|
+
exist. In this schema example, `<version>` stands for the package's version:
|
|
89
89
|
|
|
90
90
|
```json
|
|
91
91
|
{
|
|
@@ -99,6 +99,14 @@ schema example, `<version>` stands for the package's version:
|
|
|
99
99
|
"url": "https://github.com/agentshouse/kit/releases/download/v<version>/connect-linux.sh"
|
|
100
100
|
},
|
|
101
101
|
"workspace": { "argument": "--workspace", "quoting": "posix" }
|
|
102
|
+
},
|
|
103
|
+
"windows": {
|
|
104
|
+
"command": "& ([scriptblock]::Create((irm https://github.com/agentshouse/kit/releases/download/v<version>/connect-windows.ps1)))",
|
|
105
|
+
"asset": {
|
|
106
|
+
"name": "connect-windows.ps1",
|
|
107
|
+
"url": "https://github.com/agentshouse/kit/releases/download/v<version>/connect-windows.ps1"
|
|
108
|
+
},
|
|
109
|
+
"workspace": { "argument": "--workspace", "quoting": "powershell" }
|
|
102
110
|
}
|
|
103
111
|
}
|
|
104
112
|
}
|
|
@@ -107,10 +115,13 @@ schema example, `<version>` stands for the package's version:
|
|
|
107
115
|
`command` is complete for the default workspace. To show a selected absolute
|
|
108
116
|
workspace, append the declared `workspace.argument` and the path as one POSIX
|
|
109
117
|
shell-quoted argument. For example, `/home/user/My Agents' House` becomes
|
|
110
|
-
`--workspace '/home/user/My Agents'\'' House'`.
|
|
118
|
+
`--workspace '/home/user/My Agents'\'' House'`. A `powershell` workspace is one
|
|
119
|
+
single-quoted PowerShell string with each `'` doubled: `C:\Users\Alice\Alice's Room`
|
|
120
|
+
becomes `--workspace 'C:\Users\Alice\Alice''s Room'`. Quote the entire path before
|
|
111
121
|
adding it to a copyable shell command; never interpolate it as shell syntax.
|
|
112
|
-
|
|
122
|
+
`connect-linux.sh` validates an absolute workspace root and encodes it as one Docker
|
|
113
123
|
`--mount` CSV source field, including commas and double quotes in the name.
|
|
124
|
+
`connect-windows.ps1` refuses a workspace root containing a comma.
|
|
114
125
|
The package manifest contains no image digest: the release workflow
|
|
115
126
|
fills the immutable image digest into the published script after the package
|
|
116
127
|
has been built.
|
|
@@ -154,7 +165,9 @@ On a Mac with Apple silicon or an Intel chip, run the same
|
|
|
154
165
|
correctly even from a terminal running under Rosetta.
|
|
155
166
|
|
|
156
167
|
The bootstrap first establishes Docker Desktop from what it observes. A Docker
|
|
157
|
-
Desktop that already answers is used as it is.
|
|
168
|
+
Desktop that already answers is used as it is. Another Docker engine that
|
|
169
|
+
answers, such as Colima's or OrbStack's, is refused before Kit home exists,
|
|
170
|
+
because `docker info` does not report it as Docker Desktop. Without
|
|
158
171
|
`/Applications/Docker.app` it follows Docker's
|
|
159
172
|
[command-line installation](https://docs.docker.com/desktop/setup/install/mac-install/):
|
|
160
173
|
it downloads `Docker.dmg` for the chip from `desktop.docker.com`, attaches it
|
|
@@ -176,6 +189,67 @@ browser, and no Kit container uses the host network. A loopback House needs
|
|
|
176
189
|
Docker Desktop's host networking enabled. A `--workspace` root must be a
|
|
177
190
|
directory Docker Desktop shares, such as one under `/Users`.
|
|
178
191
|
|
|
192
|
+
## Connect a Windows computer
|
|
193
|
+
|
|
194
|
+
On Windows 11 `amd64`, run the published, versioned bootstrap once in
|
|
195
|
+
PowerShell:
|
|
196
|
+
|
|
197
|
+
```powershell
|
|
198
|
+
& ([scriptblock]::Create((irm https://github.com/agentshouse/kit/releases/download/v0.1.0-alpha.36/connect-windows.ps1)))
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
It runs in the Windows PowerShell 5.1 that Windows ships and needs no change to
|
|
202
|
+
its execution policy. It reads the edition, build and processor from Windows
|
|
203
|
+
itself and refuses Windows Server, Windows 10, a Windows 11 build older than
|
|
204
|
+
23H2 (build 22631), which [Docker Desktop requires](https://docs.docker.com/desktop/setup/install/windows-install/),
|
|
205
|
+
and Windows on ARM, each with its cause. There is no native Windows Kit and no
|
|
206
|
+
Windows container.
|
|
207
|
+
|
|
208
|
+
The bootstrap then establishes Docker Desktop and its WSL 2 engine from what it
|
|
209
|
+
observes. A Docker Desktop that already answers with Linux containers on its
|
|
210
|
+
WSL 2 engine is used as it is. Otherwise, when `wsl --version` does not answer,
|
|
211
|
+
it runs Microsoft's [`wsl --install --no-distribution`](https://learn.microsoft.com/windows/wsl/basic-commands#install),
|
|
212
|
+
which asks you to approve administrator rights in the ordinary Windows prompt,
|
|
213
|
+
and stops until you restart Windows; a WSL older than the 2.1.5 Docker Desktop
|
|
214
|
+
requires is brought up to date with `wsl --update` the same way. Without
|
|
215
|
+
Docker Desktop in `%ProgramFiles%\Docker\Docker` or
|
|
216
|
+
`%LOCALAPPDATA%\Programs\DockerDesktop`, it downloads Docker Desktop's
|
|
217
|
+
installer from `desktop.docker.com` and runs its
|
|
218
|
+
[per-user installation](https://docs.docker.com/desktop/setup/install/windows-install/#install-from-the-command-line),
|
|
219
|
+
`install --user --quiet`, which needs no administrator rights and does not
|
|
220
|
+
accept Docker's Subscription Service Agreement for you. It starts Docker
|
|
221
|
+
Desktop, including one a restart left stopped, and waits up to five minutes for
|
|
222
|
+
it to answer while you accept its terms or finish what its window asks.
|
|
223
|
+
Declined administrator rights, a pending restart, a failed download or
|
|
224
|
+
installer, or a Docker Desktop that is still not ready each stop the bootstrap
|
|
225
|
+
with their cause and change nothing further; running the same command again
|
|
226
|
+
continues from that state. Docker Desktop's Windows containers, another Docker
|
|
227
|
+
engine or Docker Desktop's Hyper-V engine are refused.
|
|
228
|
+
|
|
229
|
+
Everything after that is [the Linux journey](#connect-a-linux-computer):
|
|
230
|
+
the same immutable `linux/amd64` image, owner-only `%USERPROFILE%\.house-kit`
|
|
231
|
+
and `%USERPROFILE%\AgentsHouse` or one `--workspace` root such as
|
|
232
|
+
`--workspace 'D:\Agents'`, mounted as the Kit home and `/agents/house`,
|
|
233
|
+
identity reuse, `unless-stopped`, and House Login in the host browser. As on a
|
|
234
|
+
Mac, login's container publishes one callback port on `127.0.0.1` for an HTTPS
|
|
235
|
+
House, and a loopback House needs Docker Desktop's host networking. Kit home and
|
|
236
|
+
the workspace are restricted to your Windows account alone. A path with a comma
|
|
237
|
+
is refused, because this bootstrap cannot pass one to a Docker mount from
|
|
238
|
+
Windows PowerShell. The containers run as the image's own user, since Windows
|
|
239
|
+
has no Linux user id to map. Run it from the 64-bit Windows PowerShell; from an
|
|
240
|
+
administrator PowerShell it still makes your own account the owner of both
|
|
241
|
+
directories. Append `kit` and a command, or run `house git push … --owner`
|
|
242
|
+
from beneath the workspace, to forward it as on Linux, with each argument
|
|
243
|
+
passed unchanged; with the output redirected, a hidden prompt still echoes
|
|
244
|
+
what you type, so leave the output on the terminal when a command asks for a
|
|
245
|
+
token. Each run and forwarded command leaves its exit status in
|
|
246
|
+
`$LASTEXITCODE` and never closes the PowerShell window.
|
|
247
|
+
|
|
248
|
+
This recipe is authored from Microsoft's and Docker's documentation and checked
|
|
249
|
+
off Windows: the Windows PowerShell 5.1 syntax check and the release packaging
|
|
250
|
+
tests in this repository. No Windows computer has run it yet;
|
|
251
|
+
the one real Windows 11 installation is owed at the final MVP candidate.
|
|
252
|
+
|
|
179
253
|
## Building the package
|
|
180
254
|
|
|
181
255
|
The repository checkout is the development source. Build the package that can
|
package/bin/connect-linux.sh
CHANGED
|
@@ -228,6 +228,8 @@ esac
|
|
|
228
228
|
[[ "$WORKSPACE" == /* ]] || refuse 'workspace root must be an absolute path'
|
|
229
229
|
if [[ "$RUNTIME" == 'Docker Desktop' ]]; then
|
|
230
230
|
establish_docker_desktop "$CHIP"
|
|
231
|
+
DOCKER_SYSTEM=$(docker info --format '{{.OperatingSystem}}' 2>/dev/null) || refuse "Docker Desktop does not answer: $(docker_cause)"
|
|
232
|
+
[[ "$DOCKER_SYSTEM" == 'Docker Desktop' ]] || refuse "Docker answers from $DOCKER_SYSTEM, not Docker Desktop; stop that engine or switch Docker to Docker Desktop, then rerun this bootstrap"
|
|
231
233
|
else
|
|
232
234
|
establish_docker_engine
|
|
233
235
|
fi
|
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
$ErrorActionPreference = 'Stop'
|
|
2
|
+
$ProgressPreference = 'SilentlyContinue'
|
|
3
|
+
|
|
4
|
+
$Image = 'ghcr.io/agentshouse/kit@sha256:__IMAGE_DIGEST__'
|
|
5
|
+
$Name = 'house-kit'
|
|
6
|
+
$BootstrapArguments = @($args | ForEach-Object { [string]$_ })
|
|
7
|
+
$MinimumBuild = 22631
|
|
8
|
+
$MinimumWsl = [version]'2.1.5'
|
|
9
|
+
$DesktopInstaller = 'https://desktop.docker.com/win/main/amd64/Docker%20Desktop%20Installer.exe'
|
|
10
|
+
$DesktopWaitSeconds = 300
|
|
11
|
+
|
|
12
|
+
function Stop-Bootstrap([string] $Cause) {
|
|
13
|
+
throw "kit_bootstrap_refused: $Cause"
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function Exit-Bootstrap([int] $Status) {
|
|
17
|
+
throw "kit_bootstrap_exit: $Status"
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function Invoke-Native([string] $Command, [string[]] $NativeArguments) {
|
|
21
|
+
$ErrorActionPreference = 'Continue'
|
|
22
|
+
$output = @(& $Command @NativeArguments 2>&1 | ForEach-Object { "$_" })
|
|
23
|
+
[pscustomobject]@{ Status = $LASTEXITCODE; Output = $output }
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function ConvertTo-NativeArgument([string] $Value) {
|
|
27
|
+
if ((Get-Variable -Name PSNativeCommandArgumentPassing -ValueOnly -ErrorAction SilentlyContinue) -in @('Windows', 'Standard')) { return $Value }
|
|
28
|
+
if ($Value -eq '') { return '""' }
|
|
29
|
+
$escaped = $Value -replace '(\\*)"', '$1$1\"'
|
|
30
|
+
if ($PSVersionTable.PSEdition -eq 'Desktop' -and $Value -match '\s') { $escaped = $escaped -replace '(\\+)$', '$1$1' }
|
|
31
|
+
$escaped
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function Invoke-Docker {
|
|
35
|
+
& docker @args
|
|
36
|
+
if ($LASTEXITCODE -ne 0) { Exit-Bootstrap $LASTEXITCODE }
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function Read-Docker {
|
|
40
|
+
$read = [pscustomobject]@{ Answers = $false; Platform = ''; System = ''; Kernel = ''; Cause = 'the docker command is not installed' }
|
|
41
|
+
if (-not (Get-Command docker -CommandType Application -ErrorAction SilentlyContinue)) { return $read }
|
|
42
|
+
$result = Invoke-Native docker @('info', '--format', '{{.OSType}}/{{.Architecture}}|{{.OperatingSystem}}|{{.KernelVersion}}')
|
|
43
|
+
$line = @($result.Output | Where-Object { $_ -match '^[a-z]+/[a-z0-9_]+\|' })
|
|
44
|
+
if ($result.Status -ne 0 -or $line.Count -eq 0) {
|
|
45
|
+
$read.Cause = @($result.Output | Where-Object { $_ })[-1]
|
|
46
|
+
return $read
|
|
47
|
+
}
|
|
48
|
+
$fields = $line[0].Split('|')
|
|
49
|
+
$read.Answers = $true
|
|
50
|
+
$read.Platform = $fields[0]
|
|
51
|
+
$read.System = $fields[1]
|
|
52
|
+
$read.Kernel = $fields[2]
|
|
53
|
+
$read
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function Invoke-Elevated([string] $FilePath, [string[]] $ElevatedArguments, [string] $Failure, [int[]] $Accepted = @(0)) {
|
|
57
|
+
try {
|
|
58
|
+
$process = Start-Process -FilePath $FilePath -ArgumentList $ElevatedArguments -Verb RunAs -Wait -PassThru
|
|
59
|
+
} catch {
|
|
60
|
+
Stop-Bootstrap "$Failure; Windows did not grant administrator rights: $($_.Exception.Message); rerun this bootstrap and approve the prompt"
|
|
61
|
+
}
|
|
62
|
+
if ($Accepted -notcontains $process.ExitCode) {
|
|
63
|
+
Stop-Bootstrap "$Failure (exit $($process.ExitCode)); rerun this bootstrap"
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function Get-WslVersion {
|
|
68
|
+
if (-not (Get-Command wsl.exe -CommandType Application -ErrorAction SilentlyContinue)) { return $null }
|
|
69
|
+
$result = Invoke-Native wsl.exe @('--version')
|
|
70
|
+
if ($result.Status -ne 0) { return $null }
|
|
71
|
+
if ((($result.Output -join "`n") -replace "`0", '') -match '(\d+)\.(\d+)\.(\d+)') {
|
|
72
|
+
return [version]"$($Matches[1]).$($Matches[2]).$($Matches[3])"
|
|
73
|
+
}
|
|
74
|
+
$null
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function Initialize-Wsl {
|
|
78
|
+
$version = Get-WslVersion
|
|
79
|
+
if ($null -eq $version) {
|
|
80
|
+
Write-Host 'Installing WSL through wsl --install --no-distribution; Windows asks you to approve administrator rights.'
|
|
81
|
+
Invoke-Elevated 'wsl.exe' @('--install', '--no-distribution') 'WSL could not be installed' @(0, 3010)
|
|
82
|
+
Stop-Bootstrap 'WSL is installed and needs a Windows restart; restart Windows, then rerun this bootstrap'
|
|
83
|
+
}
|
|
84
|
+
if ($version -lt $MinimumWsl) {
|
|
85
|
+
Write-Host "Updating WSL $version through wsl --update; Windows asks you to approve administrator rights."
|
|
86
|
+
Invoke-Elevated 'wsl.exe' @('--update') 'WSL could not be updated'
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function Find-DockerDesktop {
|
|
91
|
+
foreach ($location in @((Join-Path $env:ProgramFiles 'Docker\Docker'), (Join-Path $env:LOCALAPPDATA 'Programs\DockerDesktop'))) {
|
|
92
|
+
if (Test-Path -LiteralPath (Join-Path $location 'Docker Desktop.exe') -PathType Leaf) { return $location }
|
|
93
|
+
}
|
|
94
|
+
$null
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function Add-DockerPath([string] $Location) {
|
|
98
|
+
if (-not (Get-Command docker -CommandType Application -ErrorAction SilentlyContinue)) {
|
|
99
|
+
$env:PATH = "$env:PATH;$(Join-Path $Location 'resources\bin')"
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function Install-DockerDesktop {
|
|
104
|
+
$download = Join-Path ([IO.Path]::GetTempPath()) ([IO.Path]::GetRandomFileName())
|
|
105
|
+
[void][IO.Directory]::CreateDirectory($download)
|
|
106
|
+
try {
|
|
107
|
+
$installer = Join-Path $download 'Docker Desktop Installer.exe'
|
|
108
|
+
Write-Host 'Downloading Docker Desktop for Windows from docker.com.'
|
|
109
|
+
try {
|
|
110
|
+
Invoke-WebRequest -UseBasicParsing -Uri $DesktopInstaller -OutFile $installer
|
|
111
|
+
} catch {
|
|
112
|
+
Stop-Bootstrap "Docker Desktop could not be downloaded: $($_.Exception.Message)"
|
|
113
|
+
}
|
|
114
|
+
Write-Host 'Installing Docker Desktop for this Windows account.'
|
|
115
|
+
try {
|
|
116
|
+
$process = Start-Process -FilePath $installer -ArgumentList @('install', '--user', '--quiet') -Wait -PassThru
|
|
117
|
+
} catch {
|
|
118
|
+
Stop-Bootstrap "the Docker Desktop installer could not be started: $($_.Exception.Message)"
|
|
119
|
+
}
|
|
120
|
+
if ($process.ExitCode -ne 0) { Stop-Bootstrap "the Docker Desktop installer did not finish (exit $($process.ExitCode))" }
|
|
121
|
+
} finally {
|
|
122
|
+
Remove-Item -LiteralPath $download -Recurse -Force -ErrorAction SilentlyContinue
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function Initialize-DockerDesktop {
|
|
127
|
+
$location = Find-DockerDesktop
|
|
128
|
+
if ($location) { Add-DockerPath $location }
|
|
129
|
+
$docker = Read-Docker
|
|
130
|
+
if ($docker.Answers) { return $docker }
|
|
131
|
+
if ($Forwarding) {
|
|
132
|
+
if (-not $location) { Stop-Bootstrap 'House Kit is not installed; run this bootstrap without arguments first' }
|
|
133
|
+
Stop-Bootstrap "Docker Desktop does not answer: $($docker.Cause); start Docker Desktop, then rerun this command"
|
|
134
|
+
}
|
|
135
|
+
Initialize-Wsl
|
|
136
|
+
if (-not $location) {
|
|
137
|
+
Install-DockerDesktop
|
|
138
|
+
$location = Find-DockerDesktop
|
|
139
|
+
if (-not $location) { Stop-Bootstrap 'Docker Desktop is not where its installer puts it; rerun this bootstrap' }
|
|
140
|
+
Add-DockerPath $location
|
|
141
|
+
}
|
|
142
|
+
Write-Host 'Starting Docker Desktop; accept its terms or finish its setup if its window asks.'
|
|
143
|
+
try {
|
|
144
|
+
Start-Process -FilePath (Join-Path $location 'Docker Desktop.exe')
|
|
145
|
+
} catch {
|
|
146
|
+
Stop-Bootstrap "Docker Desktop could not be started: $($_.Exception.Message)"
|
|
147
|
+
}
|
|
148
|
+
$waited = 0
|
|
149
|
+
while (-not ($docker = Read-Docker).Answers) {
|
|
150
|
+
if ($waited -ge $DesktopWaitSeconds) {
|
|
151
|
+
Stop-Bootstrap "Docker Desktop is not ready: $($docker.Cause); finish what its window asks, then rerun this bootstrap"
|
|
152
|
+
}
|
|
153
|
+
Start-Sleep -Seconds 2
|
|
154
|
+
$waited += 2
|
|
155
|
+
}
|
|
156
|
+
$docker
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function Protect-Directory([string] $Path) {
|
|
160
|
+
if ($Path.Contains(',')) { Stop-Bootstrap "$Path contains a comma, which this bootstrap cannot pass to a Docker mount from Windows PowerShell" }
|
|
161
|
+
$item = Get-Item -LiteralPath $Path -Force -ErrorAction SilentlyContinue
|
|
162
|
+
if ($item -and ($item.Attributes -band [IO.FileAttributes]::ReparsePoint)) { Stop-Bootstrap "$Path is a symbolic link" }
|
|
163
|
+
$created = -not $item
|
|
164
|
+
if ($created) {
|
|
165
|
+
try { [void][IO.Directory]::CreateDirectory($Path) } catch { Stop-Bootstrap "$Path could not be created" }
|
|
166
|
+
$item = Get-Item -LiteralPath $Path -Force
|
|
167
|
+
}
|
|
168
|
+
if (-not $item.PSIsContainer) { Stop-Bootstrap "$Path is not a directory" }
|
|
169
|
+
$owner = (Get-Acl -LiteralPath $Path).GetOwner([Security.Principal.SecurityIdentifier]).Value
|
|
170
|
+
if (-not $created -and $owner -ne $User.Value) { Stop-Bootstrap "$Path is not owned by this User" }
|
|
171
|
+
try {
|
|
172
|
+
$security = New-Object Security.AccessControl.DirectorySecurity
|
|
173
|
+
if ($owner -ne $User.Value) { $security.SetOwner($User) }
|
|
174
|
+
$security.SetAccessRuleProtection($true, $false)
|
|
175
|
+
$security.AddAccessRule((New-Object Security.AccessControl.FileSystemAccessRule($User, 'FullControl', 'ContainerInherit, ObjectInherit', 'None', 'Allow')))
|
|
176
|
+
$item.SetAccessControl($security)
|
|
177
|
+
} catch {
|
|
178
|
+
Stop-Bootstrap "$Path could not be owner-protected"
|
|
179
|
+
}
|
|
180
|
+
$item.FullName.TrimEnd('\')
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function Write-Text([string] $Path, [string] $Text) {
|
|
184
|
+
[IO.File]::WriteAllText($Path, $Text, (New-Object Text.UTF8Encoding($false)))
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function Read-Inspect([string] $Format) {
|
|
188
|
+
$value = & docker inspect --format $Format $Name
|
|
189
|
+
if ($LASTEXITCODE -ne 0) { Exit-Bootstrap $LASTEXITCODE }
|
|
190
|
+
"$value"
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function Read-Source([string] $Destination) {
|
|
194
|
+
$source = Read-Inspect ('{{range .Mounts}}{{if eq .Destination `' + $Destination + '`}}{{.Source}}{{end}}{{end}}')
|
|
195
|
+
if ($source -match '^/run/desktop/mnt/host/([a-z])(/.*)?$') { $source = "$($Matches[1].ToUpper()):$($Matches[2] -replace '/', '\')" }
|
|
196
|
+
$source
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function Test-Container {
|
|
200
|
+
(Invoke-Native docker @('container', 'inspect', $Name)).Status -eq 0
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function Invoke-Login([string[]] $LoginArguments) {
|
|
204
|
+
if ($LoginArguments -contains '--manual') {
|
|
205
|
+
Invoke-Docker run -i --rm @Network @Common login @LoginArguments
|
|
206
|
+
return
|
|
207
|
+
}
|
|
208
|
+
& docker run @Published --rm @Network @Common login @LoginArguments --host-browser | ForEach-Object {
|
|
209
|
+
if ($_ -like 'Open *') {
|
|
210
|
+
Write-Host 'Opening House Login in the host browser.'
|
|
211
|
+
try { Start-Process -FilePath $_.Substring(5) } catch { Stop-Bootstrap 'host browser did not open; rerun with --manual' }
|
|
212
|
+
} else {
|
|
213
|
+
Write-Host $_
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
if ($LASTEXITCODE -ne 0) { Exit-Bootstrap $LASTEXITCODE }
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function Start-Resident {
|
|
220
|
+
$resident = @()
|
|
221
|
+
if (-not $House.StartsWith('https://')) { $resident = @('--network', 'host') }
|
|
222
|
+
Invoke-Docker run -d --name $Name --restart unless-stopped @resident --mount $KitHomeMount --mount $WorkspaceMount --label "agentshouse.house=$House" --env HOME=/kit-home --env HOUSE_KIT_HOME=/kit-home --env HOUSE_KIT_CONTAINER=1 $Image resident | Out-Null
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function Connect-Kit {
|
|
226
|
+
$House = 'https://agents.house'
|
|
227
|
+
$Workspace = $null
|
|
228
|
+
$Manual = $false
|
|
229
|
+
$Skills = 'true'
|
|
230
|
+
$WorkspaceSelected = $false
|
|
231
|
+
$HouseSelected = $false
|
|
232
|
+
$Forwarding = $false
|
|
233
|
+
$Forwarded = 'kit'
|
|
234
|
+
$Forward = @()
|
|
235
|
+
$Arguments = $BootstrapArguments
|
|
236
|
+
if ($Arguments.Count -gt 0 -and ($Arguments[0] -ceq 'kit' -or $Arguments[0] -ceq 'house')) {
|
|
237
|
+
$Forwarding = $true
|
|
238
|
+
$Forwarded = $Arguments[0]
|
|
239
|
+
$Forward = @($Arguments | Select-Object -Skip 1 | ForEach-Object { ConvertTo-NativeArgument $_ })
|
|
240
|
+
$Arguments = @()
|
|
241
|
+
}
|
|
242
|
+
for ($index = 0; $index -lt $Arguments.Count; $index++) {
|
|
243
|
+
$argument = $Arguments[$index]
|
|
244
|
+
if ($argument -ceq '--workspace' -or $argument -ceq '--house') {
|
|
245
|
+
if ($index + 1 -ge $Arguments.Count) { Stop-Bootstrap "$argument requires one value" }
|
|
246
|
+
$index++
|
|
247
|
+
if ($argument -ceq '--workspace') {
|
|
248
|
+
if ($WorkspaceSelected) { Stop-Bootstrap 'supply exactly one workspace root' }
|
|
249
|
+
$Workspace = $Arguments[$index]
|
|
250
|
+
$WorkspaceSelected = $true
|
|
251
|
+
} else {
|
|
252
|
+
if ($HouseSelected) { Stop-Bootstrap 'supply exactly one House origin' }
|
|
253
|
+
$House = $Arguments[$index]
|
|
254
|
+
$HouseSelected = $true
|
|
255
|
+
}
|
|
256
|
+
} elseif ($argument -ceq '--manual') {
|
|
257
|
+
$Manual = $true
|
|
258
|
+
} elseif ($argument -ceq '--no-skills') {
|
|
259
|
+
$Skills = 'false'
|
|
260
|
+
} else {
|
|
261
|
+
Stop-Bootstrap "unknown argument $argument"
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if ($Image -notmatch '^ghcr\.io/agentshouse/kit@sha256:[0-9a-f]{64}$') { Stop-Bootstrap 'this bootstrap has not been published with an immutable image digest' }
|
|
266
|
+
if ($env:OS -ne 'Windows_NT') { Stop-Bootstrap 'this bootstrap is for Windows 11; run connect-linux.sh on Linux and macOS' }
|
|
267
|
+
if (-not [Environment]::Is64BitProcess) { Stop-Bootstrap 'this is a 32-bit PowerShell; run this bootstrap from the 64-bit Windows PowerShell' }
|
|
268
|
+
$system = Get-CimInstance -ClassName Win32_OperatingSystem
|
|
269
|
+
if ([int]$system.ProductType -ne 1) { Stop-Bootstrap "$($system.Caption) is a Windows Server edition; this bootstrap supports Windows 11 on amd64" }
|
|
270
|
+
if ([int]$system.BuildNumber -lt 22000) { Stop-Bootstrap "$($system.Caption) is not Windows 11; this bootstrap supports Windows 11 on amd64" }
|
|
271
|
+
if ([int]$system.BuildNumber -lt $MinimumBuild) { Stop-Bootstrap "Docker Desktop needs Windows 11 23H2 (build $MinimumBuild) or later, not build $($system.BuildNumber); update Windows, then rerun this bootstrap" }
|
|
272
|
+
$processor = [int]@(Get-CimInstance -ClassName Win32_Processor)[0].Architecture
|
|
273
|
+
if ($processor -ne 9) {
|
|
274
|
+
$described = @{ 0 = 'x86'; 5 = 'ARM'; 12 = 'ARM64' }[$processor]
|
|
275
|
+
if (-not $described) { $described = "architecture $processor" }
|
|
276
|
+
Stop-Bootstrap "Windows on $described is not supported; this bootstrap supports Windows 11 on amd64"
|
|
277
|
+
}
|
|
278
|
+
if ($WorkspaceSelected -and $Workspace -notmatch '^[A-Za-z]:\\') { Stop-Bootstrap 'workspace root must be an absolute path' }
|
|
279
|
+
|
|
280
|
+
$docker = Initialize-DockerDesktop
|
|
281
|
+
if (-not $docker.Platform.StartsWith('linux/')) { Stop-Bootstrap "Docker runs $($docker.Platform.Split('/')[0]) containers; switch Docker Desktop to Linux containers, then rerun this bootstrap" }
|
|
282
|
+
if ($docker.System -ne 'Docker Desktop') { Stop-Bootstrap "Docker answers from $($docker.System), not Docker Desktop; stop that engine or switch Docker to Docker Desktop, then rerun this bootstrap" }
|
|
283
|
+
if ($docker.Kernel -notlike '*WSL2*') { Stop-Bootstrap "Docker Desktop does not use its WSL 2 engine; turn on Use the WSL 2 based engine in its settings, then rerun this bootstrap" }
|
|
284
|
+
if ($docker.Platform -ne 'linux/x86_64' -and $docker.Platform -ne 'linux/amd64') { Stop-Bootstrap "host linux/amd64 does not match Docker Desktop $($docker.Platform)" }
|
|
285
|
+
if ($Forwarding) {
|
|
286
|
+
if (-not (Test-Container)) { Stop-Bootstrap 'House Kit is not installed; run this bootstrap without arguments first' }
|
|
287
|
+
$Workspace = Read-Source '/agents/house'
|
|
288
|
+
if ($Workspace -notmatch '^[A-Za-z]:\\' -or -not (Test-Path -LiteralPath $Workspace -PathType Container)) {
|
|
289
|
+
Stop-Bootstrap "the installed Kit's workspace root $Workspace is not a directory on this computer; rerun this bootstrap without arguments"
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
if (-not $Workspace) { $Workspace = Join-Path $env:USERPROFILE 'AgentsHouse' }
|
|
293
|
+
|
|
294
|
+
$User = [Security.Principal.WindowsIdentity]::GetCurrent().User
|
|
295
|
+
$kitHome = Protect-Directory (Join-Path $env:USERPROFILE '.house-kit')
|
|
296
|
+
$Workspace = Protect-Directory $Workspace
|
|
297
|
+
$KitHomeMount = "type=bind,src=$kitHome,dst=/kit-home"
|
|
298
|
+
$WorkspaceMount = "type=bind,src=$Workspace,dst=/agents/house"
|
|
299
|
+
$credential = Join-Path $kitHome 'credential'
|
|
300
|
+
$environment = Join-Path $kitHome 'environment'
|
|
301
|
+
$bound = Join-Path $kitHome 'house'
|
|
302
|
+
$configuration = Join-Path $kitHome 'kit.json'
|
|
303
|
+
if (((Test-Path -LiteralPath $credential) -or (Test-Path -LiteralPath $environment)) -and -not (Test-Path -LiteralPath $bound)) {
|
|
304
|
+
Stop-Bootstrap 'stored House origin is missing for an enrolled Environment'
|
|
305
|
+
}
|
|
306
|
+
if (Test-Path -LiteralPath $bound) {
|
|
307
|
+
$boundHouse = @([IO.File]::ReadAllLines($bound))
|
|
308
|
+
if ($boundHouse.Count -eq 0) { Stop-Bootstrap 'stored House origin is unreadable' }
|
|
309
|
+
if ($HouseSelected -and $House -cne $boundHouse[0]) { Stop-Bootstrap 'this Environment is bound to another House origin' }
|
|
310
|
+
$House = $boundHouse[0]
|
|
311
|
+
}
|
|
312
|
+
if ($House -notmatch '^(https://|http://127\.0\.0\.1:|http://localhost:)') { Stop-Bootstrap 'House origin must use HTTPS or local loopback HTTP' }
|
|
313
|
+
if (-not $Forwarding) {
|
|
314
|
+
try {
|
|
315
|
+
if (-not (Test-Path -LiteralPath $configuration)) {
|
|
316
|
+
Write-Text $configuration "{`"availability`":`"intermittent`",`"execution_authority`":`"allow`",`"migrators`":[],`"instances`":[],`"clients`":[],`"host`":null,`"skills`":$Skills}`n"
|
|
317
|
+
} else {
|
|
318
|
+
$text = [IO.File]::ReadAllText($configuration)
|
|
319
|
+
if ($text -match '"skills"\s*:') {
|
|
320
|
+
$text = $text -replace '("skills"\s*:\s*)(true|false)', "`${1}$Skills"
|
|
321
|
+
} else {
|
|
322
|
+
$text = $text -replace '^(\s*)\{', "`${1}{`"skills`":$Skills,"
|
|
323
|
+
}
|
|
324
|
+
Write-Text $configuration $text
|
|
325
|
+
}
|
|
326
|
+
} catch {
|
|
327
|
+
Stop-Bootstrap 'kit.json could not be updated'
|
|
328
|
+
}
|
|
329
|
+
if ([IO.File]::ReadAllText($configuration) -notmatch "`"skills`"\s*:\s*$Skills") { Stop-Bootstrap 'kit.json could not be updated' }
|
|
330
|
+
}
|
|
331
|
+
$Network = @('--network', 'host')
|
|
332
|
+
$Published = @()
|
|
333
|
+
if ($House.StartsWith('https://')) {
|
|
334
|
+
$Network = @()
|
|
335
|
+
$port = Get-Random -Minimum 49152 -Maximum 65536
|
|
336
|
+
$Published = @('--publish', "127.0.0.1:${port}:$port", '--env', "HOUSE_KIT_LOGIN_PORT=$port")
|
|
337
|
+
}
|
|
338
|
+
$Common = @('--mount', $KitHomeMount, '--mount', $WorkspaceMount, '--env', 'HOME=/kit-home', '--env', 'HOUSE_KIT_HOME=/kit-home', '--env', 'HOUSE_KIT_CONTAINER=1', '--env', "HOUSE_KIT_HOSTNAME=$([Net.Dns]::GetHostName())", $Image)
|
|
339
|
+
|
|
340
|
+
if (Test-Container) {
|
|
341
|
+
$installed = Read-Inspect '{{.Config.Image}}'
|
|
342
|
+
if ($Forwarding -and $installed -cne $Image) { Stop-Bootstrap 'an installed Kit uses a different image; rerun this bootstrap without arguments to update it' }
|
|
343
|
+
if ((Read-Inspect '{{.HostConfig.RestartPolicy.Name}}') -cne 'unless-stopped') { Stop-Bootstrap 'the installed Kit has a different lifecycle' }
|
|
344
|
+
if ((Read-Inspect '{{len .Mounts}}') -cne '2') { Stop-Bootstrap 'the installed Kit has unexpected mounts' }
|
|
345
|
+
if ((Read-Source '/kit-home') -ine $kitHome) { Stop-Bootstrap 'the installed Kit uses another Kit home' }
|
|
346
|
+
if ((Read-Inspect '{{range .Mounts}}{{if eq .Destination `/kit-home`}}{{.Type}}/{{.RW}}{{end}}{{end}}') -cne 'bind/true') { Stop-Bootstrap 'the installed Kit home is not a writable bind mount' }
|
|
347
|
+
if ((Read-Source '/agents/house') -ine $Workspace) { Stop-Bootstrap 'the installed Kit uses another workspace root' }
|
|
348
|
+
if ((Read-Inspect '{{range .Mounts}}{{if eq .Destination `/agents/house`}}{{.Type}}/{{.RW}}{{end}}{{end}}') -cne 'bind/true') { Stop-Bootstrap 'the installed workspace is not a writable bind mount' }
|
|
349
|
+
if ((Read-Inspect '{{index .Config.Labels `agentshouse.house`}}') -cne $House) { Stop-Bootstrap 'the installed Kit uses another House origin' }
|
|
350
|
+
if (-not ((Test-Path -LiteralPath $credential) -and (Test-Path -LiteralPath $environment) -and (Test-Path -LiteralPath $bound))) { Stop-Bootstrap 'the installed Kit has no enrolled authority' }
|
|
351
|
+
if ($Forwarding) {
|
|
352
|
+
if ($Forwarded -ceq 'kit' -and $Forward.Count -gt 0 -and $Forward[0] -ceq 'login') {
|
|
353
|
+
Invoke-Login @($Forward | Select-Object -Skip 1)
|
|
354
|
+
return
|
|
355
|
+
}
|
|
356
|
+
$placed = @()
|
|
357
|
+
$entry = @()
|
|
358
|
+
if ($Forwarded -ceq 'house') {
|
|
359
|
+
$here = (Get-Location).ProviderPath.TrimEnd('\')
|
|
360
|
+
if ($here -ine $Workspace -and -not $here.StartsWith("$Workspace\", [StringComparison]::OrdinalIgnoreCase)) {
|
|
361
|
+
Stop-Bootstrap "the current directory is outside the workspace $Workspace"
|
|
362
|
+
}
|
|
363
|
+
$placed = @('--workdir', "/agents/house$($here.Substring($Workspace.Length).Replace('\', '/'))")
|
|
364
|
+
$entry = @('--entrypoint', 'house')
|
|
365
|
+
}
|
|
366
|
+
$terminal = @()
|
|
367
|
+
if (-not [Console]::IsInputRedirected -and -not [Console]::IsOutputRedirected) { $terminal = @('-t') }
|
|
368
|
+
if ((Read-Inspect '{{.State.Running}}') -ceq 'true') {
|
|
369
|
+
& docker exec -i @terminal @placed $Name $Forwarded @Forward
|
|
370
|
+
} else {
|
|
371
|
+
& docker run -i @terminal @placed @entry --rm @Network @Common @Forward
|
|
372
|
+
}
|
|
373
|
+
Exit-Bootstrap $LASTEXITCODE
|
|
374
|
+
}
|
|
375
|
+
if ($installed -cne $Image) {
|
|
376
|
+
Invoke-Docker pull --quiet --platform linux/amd64 $Image | Out-Null
|
|
377
|
+
if ((Read-Inspect '{{.State.Running}}') -ceq 'true') { Invoke-Docker exec $Name kit resident drain }
|
|
378
|
+
Invoke-Docker rm -f $Name | Out-Null
|
|
379
|
+
Remove-Item -LiteralPath (Join-Path $kitHome 'state\update-drain') -Force -ErrorAction SilentlyContinue
|
|
380
|
+
Start-Resident
|
|
381
|
+
Write-Host "House Kit updated for Environment $([IO.File]::ReadAllText($environment).Trim())."
|
|
382
|
+
return
|
|
383
|
+
}
|
|
384
|
+
Invoke-Docker run --rm @Network @Common credential check
|
|
385
|
+
if ((Read-Inspect '{{.State.Running}}') -ceq 'true') {
|
|
386
|
+
Write-Host "House Kit is already running for Environment $([IO.File]::ReadAllText($environment).Trim())."
|
|
387
|
+
} else {
|
|
388
|
+
Invoke-Docker start $Name | Out-Null
|
|
389
|
+
Write-Host "House Kit restarted for Environment $([IO.File]::ReadAllText($environment).Trim())."
|
|
390
|
+
}
|
|
391
|
+
return
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
Invoke-Docker pull --quiet --platform linux/amd64 $Image | Out-Null
|
|
395
|
+
if (Test-Path -LiteralPath $credential) {
|
|
396
|
+
Invoke-Docker run --rm @Network @Common credential check
|
|
397
|
+
} else {
|
|
398
|
+
$login = @()
|
|
399
|
+
if ($HouseSelected) { $login += @('--house', $House) }
|
|
400
|
+
if ($Manual) { $login += '--manual' }
|
|
401
|
+
Invoke-Login $login
|
|
402
|
+
}
|
|
403
|
+
Invoke-Docker run --rm @Network @Common apply
|
|
404
|
+
Start-Resident
|
|
405
|
+
Write-Host "House Kit connected for Environment $([IO.File]::ReadAllText($environment).Trim())."
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
$consoleEncoding = [Console]::OutputEncoding
|
|
409
|
+
try {
|
|
410
|
+
try {
|
|
411
|
+
[Console]::OutputEncoding = New-Object Text.UTF8Encoding($false)
|
|
412
|
+
} catch {
|
|
413
|
+
Stop-Bootstrap 'this PowerShell has no console; run this bootstrap from a Windows PowerShell console window'
|
|
414
|
+
}
|
|
415
|
+
Connect-Kit
|
|
416
|
+
$global:LASTEXITCODE = 0
|
|
417
|
+
} catch {
|
|
418
|
+
$message = "$($_.Exception.Message)"
|
|
419
|
+
if ($message -match '^kit_bootstrap_exit: (\d+)$') {
|
|
420
|
+
$global:LASTEXITCODE = [int]$Matches[1]
|
|
421
|
+
} elseif ($message.StartsWith('kit_bootstrap_refused: ')) {
|
|
422
|
+
[Console]::Error.WriteLine($message)
|
|
423
|
+
$global:LASTEXITCODE = 1
|
|
424
|
+
} else {
|
|
425
|
+
$global:LASTEXITCODE = 1
|
|
426
|
+
throw
|
|
427
|
+
}
|
|
428
|
+
} finally {
|
|
429
|
+
try { [Console]::OutputEncoding = $consoleEncoding } catch { }
|
|
430
|
+
}
|
package/bootstrap.json
CHANGED
|
@@ -1,17 +1,28 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.36",
|
|
4
4
|
"hosts": {
|
|
5
5
|
"linux": {
|
|
6
|
-
"command": "bash <(curl -fsSL https://github.com/agentshouse/kit/releases/download/v0.1.0-alpha.
|
|
6
|
+
"command": "bash <(curl -fsSL https://github.com/agentshouse/kit/releases/download/v0.1.0-alpha.36/connect-linux.sh)",
|
|
7
7
|
"asset": {
|
|
8
8
|
"name": "connect-linux.sh",
|
|
9
|
-
"url": "https://github.com/agentshouse/kit/releases/download/v0.1.0-alpha.
|
|
9
|
+
"url": "https://github.com/agentshouse/kit/releases/download/v0.1.0-alpha.36/connect-linux.sh"
|
|
10
10
|
},
|
|
11
11
|
"workspace": {
|
|
12
12
|
"argument": "--workspace",
|
|
13
13
|
"quoting": "posix"
|
|
14
14
|
}
|
|
15
|
+
},
|
|
16
|
+
"windows": {
|
|
17
|
+
"command": "& ([scriptblock]::Create((irm https://github.com/agentshouse/kit/releases/download/v0.1.0-alpha.36/connect-windows.ps1)))",
|
|
18
|
+
"asset": {
|
|
19
|
+
"name": "connect-windows.ps1",
|
|
20
|
+
"url": "https://github.com/agentshouse/kit/releases/download/v0.1.0-alpha.36/connect-windows.ps1"
|
|
21
|
+
},
|
|
22
|
+
"workspace": {
|
|
23
|
+
"argument": "--workspace",
|
|
24
|
+
"quoting": "powershell"
|
|
25
|
+
}
|
|
15
26
|
}
|
|
16
27
|
}
|
|
17
28
|
}
|