@agentshouse/kit 0.1.0-alpha.35 → 0.1.0-alpha.37
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 +39 -2204
- package/bin/connect-linux.sh +2 -0
- package/bin/connect-windows.ps1 +430 -0
- package/bootstrap.json +14 -3
- package/docs/configuration.md +104 -0
- package/docs/google.md +33 -0
- package/docs/linux.md +46 -0
- package/docs/macos.md +26 -0
- package/docs/telegram.md +29 -0
- package/docs/windows.md +37 -0
- package/package.json +1 -1
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.37",
|
|
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.37/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.37/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.37/connect-windows.ps1)))",
|
|
18
|
+
"asset": {
|
|
19
|
+
"name": "connect-windows.ps1",
|
|
20
|
+
"url": "https://github.com/agentshouse/kit/releases/download/v0.1.0-alpha.37/connect-windows.ps1"
|
|
21
|
+
},
|
|
22
|
+
"workspace": {
|
|
23
|
+
"argument": "--workspace",
|
|
24
|
+
"quoting": "powershell"
|
|
25
|
+
}
|
|
15
26
|
}
|
|
16
27
|
}
|
|
17
28
|
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# Configuration
|
|
2
|
+
|
|
3
|
+
`kit.json` in Kit home is the one configuration file. Every section below is
|
|
4
|
+
required; a file that omits one is refused.
|
|
5
|
+
|
|
6
|
+
```json
|
|
7
|
+
{
|
|
8
|
+
"availability": "continuous",
|
|
9
|
+
"execution_authority": "allow",
|
|
10
|
+
"migrators": [],
|
|
11
|
+
"instances": [
|
|
12
|
+
{
|
|
13
|
+
"id": "acme",
|
|
14
|
+
"preset": "custom",
|
|
15
|
+
"services": [
|
|
16
|
+
{
|
|
17
|
+
"service": "notes",
|
|
18
|
+
"source_ref": "src_acme_notes",
|
|
19
|
+
"rooms": ["work-room"],
|
|
20
|
+
"backfill": { "days": 30 },
|
|
21
|
+
"connector": {
|
|
22
|
+
"command": ["/usr/local/bin/acme-export", "--ndjson"],
|
|
23
|
+
"timeout_ms": 120000,
|
|
24
|
+
"environment": { "ACME_REGION": "eu" },
|
|
25
|
+
"secrets": {
|
|
26
|
+
"ACME_TOKEN": { "environment": "ACME_TOKEN_SOURCE" },
|
|
27
|
+
"ACME_KEY": { "file": "/run/secrets/acme-key" }
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
]
|
|
32
|
+
}
|
|
33
|
+
],
|
|
34
|
+
"clients": [
|
|
35
|
+
{
|
|
36
|
+
"kind": "claude-code",
|
|
37
|
+
"settings": "/home/ada/.claude/settings.json"
|
|
38
|
+
}
|
|
39
|
+
],
|
|
40
|
+
"host": null,
|
|
41
|
+
"skills": true
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
- `availability`: `"continuous"` for a computer that stays online,
|
|
46
|
+
`"intermittent"` for a laptop.
|
|
47
|
+
- `execution_authority`: `"allow"` or `"refuse"`, this computer's answer to
|
|
48
|
+
every permission request an agent makes.
|
|
49
|
+
- `instances`: your own sources, each with a `custom` connector (below).
|
|
50
|
+
- `clients`: agent clients outside Kit whose settings get House's session-start
|
|
51
|
+
hook. `claude-code` is the one supported kind; declare `[]` for none.
|
|
52
|
+
- `migrators`: `[]` unless you run a migrator.
|
|
53
|
+
- `host`: `null` unless this computer reports its capacity to House.
|
|
54
|
+
- `skills`: `true` accepts the Skill set, `false` declines it. The connect
|
|
55
|
+
command sets it.
|
|
56
|
+
|
|
57
|
+
Agents and agent CLIs are chosen in House, not here. Send your changes to
|
|
58
|
+
House with:
|
|
59
|
+
|
|
60
|
+
```sh
|
|
61
|
+
kit apply
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Secrets
|
|
65
|
+
|
|
66
|
+
Never write a secret into `kit.json`. Under `secrets`, name an environment
|
|
67
|
+
variable (`{ "environment": "NAME" }`) or a file (`{ "file": "/path" }`); Kit
|
|
68
|
+
passes its value to the connector only.
|
|
69
|
+
|
|
70
|
+
## Custom connectors
|
|
71
|
+
|
|
72
|
+
A connector is your own command. Kit runs it without a shell, with only `PATH`,
|
|
73
|
+
`environment` and the resolved secrets, and never gives it a House credential.
|
|
74
|
+
It must finish within `timeout_ms` (at most 120000).
|
|
75
|
+
|
|
76
|
+
Kit writes one request line to its standard input:
|
|
77
|
+
|
|
78
|
+
```json
|
|
79
|
+
{ "contract": "2026-08-22", "instance": "acme", "cursor": null, "limit": 100 }
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
The command prints NDJSON: `item` lines, then at most one `cursor` line.
|
|
83
|
+
`cursor` in the next request is the last one it printed.
|
|
84
|
+
|
|
85
|
+
```
|
|
86
|
+
{"type":"item","namespace":"acme/notes","native_object_id":"note-91","native_version":"7","observed_at":"2026-08-20T09:15:00.000Z","body":"the note","author":"ada","url":"https://acme.example/notes/91","attachments":[{"url":"https://acme.example/files/1","name":"plan.pdf"}]}
|
|
87
|
+
{"type":"cursor","cursor":"page-2"}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
`namespace`, `native_object_id`, `observed_at` and `body` are required. An
|
|
91
|
+
attachment is a `url`, or a local `file` with its exact `path`, `bytes`,
|
|
92
|
+
`sha256` and optional `media_type`. A malformed line, a non-zero exit or a
|
|
93
|
+
timeout rejects the whole run and nothing is delivered.
|
|
94
|
+
|
|
95
|
+
`backfill` bounds the first capture: `{ "days": 30 }`, `{ "count": 500 }` or
|
|
96
|
+
`{ "all": true }`.
|
|
97
|
+
|
|
98
|
+
```sh
|
|
99
|
+
kit source capture acme # capture every service of an instance now
|
|
100
|
+
kit source capture acme notes # one service
|
|
101
|
+
kit source status acme # health, cursor, gaps and unsaved files
|
|
102
|
+
kit source retry-files acme # save files a capture could not save
|
|
103
|
+
kit source reset acme notes # start again from the backfill boundary
|
|
104
|
+
```
|
package/docs/google.md
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Google sources
|
|
2
|
+
|
|
3
|
+
You add Gmail, Calendar and Drive sources in the House App: an account name you
|
|
4
|
+
choose, the service, the Rooms it goes to, and how much history the first
|
|
5
|
+
capture takes. Nothing is configured on this computer.
|
|
6
|
+
|
|
7
|
+
Each service captures, read-only:
|
|
8
|
+
|
|
9
|
+
- Gmail: every message except Spam and Trash.
|
|
10
|
+
- Calendar: the account's primary calendar.
|
|
11
|
+
- Drive: the files the account owns.
|
|
12
|
+
|
|
13
|
+
## Signing in
|
|
14
|
+
|
|
15
|
+
Kit ships no Google OAuth client. Each account signs in with your own:
|
|
16
|
+
|
|
17
|
+
1. In your Google Cloud project, create an OAuth client of type **Desktop app**
|
|
18
|
+
and enable the API of each service you use.
|
|
19
|
+
2. When House asks, enter the client id and secret on its page.
|
|
20
|
+
3. Approve Google's consent. Google then sends the browser to a
|
|
21
|
+
`http://127.0.0.1:<port>/` address that does not load; copy that whole
|
|
22
|
+
address and paste it into the House page.
|
|
23
|
+
|
|
24
|
+
Kit then captures each signed-in source every 15 minutes.
|
|
25
|
+
|
|
26
|
+
```sh
|
|
27
|
+
kit source capture work # capture every source of the account now
|
|
28
|
+
kit source capture work gmail # one service
|
|
29
|
+
kit source status work
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
If `kit source status` reports `api_disabled`, enable the API it names in your
|
|
33
|
+
Google Cloud project.
|
package/docs/linux.md
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# Linux
|
|
2
|
+
|
|
3
|
+
Run the Linux command from the [README](../README.md#connect-a-computer) in a
|
|
4
|
+
terminal. It needs `bash`, `curl` and Docker Engine.
|
|
5
|
+
|
|
6
|
+
## Docker Engine
|
|
7
|
+
|
|
8
|
+
- A Docker Engine that already answers is used as it is, on any distribution.
|
|
9
|
+
- On Ubuntu 22.04, 24.04 or 26.04 LTS, anything missing is installed from
|
|
10
|
+
Docker's own apt repository through `sudo`, which asks for your password.
|
|
11
|
+
The command starts the `docker` service and adds you to the `docker` group.
|
|
12
|
+
- Ubuntu packages that conflict with Docker's (`docker.io`, `docker-compose`,
|
|
13
|
+
`docker-compose-v2`, `docker-doc`, `docker-buildx`, `podman-docker`,
|
|
14
|
+
`containerd`, `runc`) are not removed for you: remove them and run again.
|
|
15
|
+
- On any other distribution, install Docker Engine first.
|
|
16
|
+
|
|
17
|
+
A new `docker` group membership applies from your next sign-in. If a later run
|
|
18
|
+
says so, sign out and back in, then run the command again.
|
|
19
|
+
|
|
20
|
+
## What it creates
|
|
21
|
+
|
|
22
|
+
- `~/.house-kit`: Kit home, with your credential and `kit.json`.
|
|
23
|
+
- `~/AgentsHouse`: your workspace, mounted in the container as
|
|
24
|
+
`/agents/house`. Pass `--workspace /absolute/path` to use another directory.
|
|
25
|
+
|
|
26
|
+
Both are readable only by you. The credential is never printed.
|
|
27
|
+
|
|
28
|
+
## Updating
|
|
29
|
+
|
|
30
|
+
Run a newer release's command. While an Agent session is still running, the
|
|
31
|
+
update names it and leaves Kit as it is; run the command again once it ends.
|
|
32
|
+
Your credential, Kit home and workspace are kept.
|
|
33
|
+
|
|
34
|
+
## Pushing a working copy
|
|
35
|
+
|
|
36
|
+
From a directory under your workspace, such as a Room copy under
|
|
37
|
+
`working-copies/rooms/`, run:
|
|
38
|
+
|
|
39
|
+
```sh
|
|
40
|
+
bash <(curl -fsSL …/connect-linux.sh) house git push --owner
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## A House on this computer
|
|
44
|
+
|
|
45
|
+
`--house http://127.0.0.1:<port>` or `--house http://localhost:<port>` runs Kit
|
|
46
|
+
on the host network so it can reach a House served on this computer.
|