@p10i/create-nestbox 0.1.2 → 0.3.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/tests/nestbox.ps1 CHANGED
@@ -91,6 +91,11 @@ function Invoke-Http([string]$Path, [string]$HostHeader = '', [string]$Method =
91
91
 
92
92
  try {
93
93
  New-Item -ItemType Directory -Path $TempDirectory | Out-Null
94
+ $configuredTokenFile = [regex]::Match([IO.File]::ReadAllText((Join-Path $Root '.env')), '(?m)^NESTBOX_HOST_TOKEN_FILE=(.+)$').Groups[1].Value.Trim()
95
+ if (-not $env:NESTBOX_HOST_TOKEN_FILE -and -not $configuredTokenFile) {
96
+ $env:NESTBOX_HOST_TOKEN_FILE = Join-Path $TempDirectory 'host-token'
97
+ [IO.File]::WriteAllText($env:NESTBOX_HOST_TOKEN_FILE, 'test-token-' + ('0' * 54))
98
+ }
94
99
 
95
100
  if (Get-Command docker -ErrorAction SilentlyContinue) { Pass 'docker is available' } else { Fail 'docker is required'; throw 'Missing Docker' }
96
101
  if (Get-Command curl.exe -ErrorAction SilentlyContinue) { Pass 'curl.exe is available' } else { Fail 'curl.exe is required'; throw 'Missing curl.exe' }
@@ -120,13 +125,15 @@ try {
120
125
  if ($openCodeInstance.Contains('Communication language:')) { Pass 'OpenCode instance policy records communication language' } else { Fail 'OpenCode instance policy omits communication language' }
121
126
  $openCodeConfig = [IO.File]::ReadAllText((Join-Path $Root 'home\configs\opencode\opencode.json'))
122
127
  if ($openCodeConfig.Contains('"instance.md"')) { Pass 'OpenCode loads instance policy directly' } else { Fail 'OpenCode does not load instance policy' }
128
+ if ($openCodeConfig.Contains('"control"')) { Pass 'OpenCode registers the control MCP' } else { Fail 'OpenCode does not register the control MCP' }
129
+ if ((Test-Path -LiteralPath (Join-Path $Root 'docker\control\server.py')) -and (Test-Path -LiteralPath (Join-Path $Root 'host-runner.mjs'))) { Pass 'Control and host runner sources exist' } else { Fail 'Control or host runner source is missing' }
123
130
 
124
131
  $result = Invoke-Compose @('config', '--quiet') -Quiet
125
132
  if ($result.Code -eq 0) { Pass 'Compose configuration is valid' } else { Fail 'Compose configuration is invalid' }
126
133
 
127
134
  $result = Invoke-Compose @('config', '--services') -Quiet
128
135
  $services = @($result.Output | ForEach-Object { $_.ToString().Trim() })
129
- foreach ($service in @('nginx', 'php-fpm', 'rollup', 'opencode')) {
136
+ foreach ($service in @('nginx', 'php-fpm', 'rollup', 'control', 'opencode')) {
130
137
  if ($services -contains $service) { Pass "Compose defines $service" } else { Fail "Compose does not define $service" }
131
138
  }
132
139
  $result = Invoke-Compose @('ps', '--services') -Quiet
@@ -135,16 +142,16 @@ try {
135
142
  }
136
143
 
137
144
  if ($Start) {
138
- $result = Invoke-Compose @('build', 'nginx', 'php-fpm', 'rollup', 'opencode')
145
+ $result = Invoke-Compose @('build', 'nginx', 'php-fpm', 'rollup', 'control', 'opencode')
139
146
  if ($result.Code -eq 0) { Pass 'All service images build' } else { Fail 'One or more service images failed to build' }
140
147
 
141
148
  # Do not start the bind-mounted Rollup watcher: it intentionally writes
142
149
  # generated bundles. Its image and output are validated separately.
143
- $result = Invoke-Compose @('up', '-d', 'nginx', 'php-fpm', 'opencode')
150
+ $result = Invoke-Compose @('up', '-d', 'nginx', 'php-fpm', 'control', 'opencode')
144
151
  if ($result.Code -eq 0) { Pass 'Application services started' } else { Fail 'Application services failed to start' }
145
152
  }
146
153
 
147
- foreach ($service in @('nginx', 'php-fpm', 'opencode')) {
154
+ foreach ($service in @('nginx', 'php-fpm', 'control', 'opencode')) {
148
155
  if (Test-ContainerRunning $service) { Pass "$service is running" } else { Fail "$service is not running" }
149
156
  }
150
157
  $rollupId = Get-ContainerId 'rollup'
@@ -155,7 +162,7 @@ try {
155
162
  }
156
163
 
157
164
  $restartCounts = @{}
158
- foreach ($service in @('nginx', 'php-fpm', 'opencode')) { $restartCounts[$service] = Get-RestartCount $service }
165
+ foreach ($service in @('nginx', 'php-fpm', 'control', 'opencode')) { $restartCounts[$service] = Get-RestartCount $service }
159
166
 
160
167
  $result = Invoke-Compose @('port', 'nginx', '80') -Quiet
161
168
  $published = ($result.Output -join "`n").Trim()
@@ -242,6 +249,18 @@ try {
242
249
  $response = Invoke-Http '/_nestbox/publish?topics=nestbox-test:public' '' 'POST'
243
250
  if ($response.Status -eq 404) { Pass 'Publisher endpoint is unavailable on the public port' } else { Fail "Public publisher endpoint returned HTTP $($response.Status)" }
244
251
 
252
+ $controlId = Get-ContainerId 'control'
253
+ $controlInspect = @(& docker inspect $controlId 2>$null | ConvertFrom-Json)[0]
254
+ if ($null -eq $controlInspect.NetworkSettings.Ports.'4088/tcp') { Pass 'Control API is not published on the host' } else { Fail 'Control API is published on the host' }
255
+ $runnerBinding = $controlInspect.NetworkSettings.Ports.'4089/tcp'
256
+ if ($null -ne $runnerBinding -and @($runnerBinding.HostIp) -notcontains '0.0.0.0' -and @($runnerBinding.HostIp) -notcontains '::') { Pass 'Host runner API is published on loopback only' } else { Fail 'Host runner API is not restricted to loopback' }
257
+ $openCodeId = Get-ContainerId 'opencode'
258
+ $openCodeInspect = @(& docker inspect $openCodeId 2>$null | ConvertFrom-Json)[0]
259
+ if (@($openCodeInspect.Mounts.Destination) -notcontains '/var/run/docker.sock') { Pass 'OpenCode does not receive the Docker socket' } else { Fail 'OpenCode receives the Docker socket' }
260
+ if (@($openCodeInspect.Mounts.Destination) -notcontains '/run/secrets/nestbox-host-token' -and @($openCodeInspect.Config.Env) -notmatch '^NESTBOX_HOST_TOKEN=') { Pass 'OpenCode cannot read the host runner token' } else { Fail 'OpenCode can read the host runner token' }
261
+ if (@($controlInspect.Mounts.Destination) -contains '/var/run/docker.sock') { Pass 'Control receives the Docker socket' } else { Fail 'Control does not receive the Docker socket' }
262
+ if (@($controlInspect.Mounts.Destination) -contains '/run/secrets/nestbox-host-token') { Pass 'Control receives the host runner token file' } else { Fail 'Control does not receive the host runner token file' }
263
+
245
264
  if ($indexPresent) {
246
265
  $result = Invoke-Compose @('exec', '-T', 'php-fpm', 'php', '-l', '/home/code/index.php') -Quiet
247
266
  if ($result.Code -eq 0) { Pass 'Installed super-document passes PHP lint' } else { Fail 'Installed super-document failed PHP lint' }
@@ -299,7 +318,7 @@ try {
299
318
  Remove-Job -Job $SseJob -Force -ErrorAction SilentlyContinue
300
319
  $SseJob = $null
301
320
 
302
- foreach ($service in @('nginx', 'php-fpm', 'opencode')) {
321
+ foreach ($service in @('nginx', 'php-fpm', 'control', 'opencode')) {
303
322
  $after = Get-RestartCount $service
304
323
  if ((Test-ContainerRunning $service) -and $null -ne $restartCounts[$service] -and $restartCounts[$service] -eq $after) { Pass "$service remained stable during the test" } else { Fail "$service restarted or stopped during the test" }
305
324
  }
package/tests/nestbox.sh CHANGED
@@ -141,6 +141,8 @@ else
141
141
  fi
142
142
 
143
143
  TEMP_DIR="$(mktemp -d "${TMPDIR:-/tmp}/nestbox-test.XXXXXX")"
144
+ configured_token_file="$(sed -n 's/^NESTBOX_HOST_TOKEN_FILE=//p' "$ROOT/.env" | tail -n 1)"
145
+ if [[ -z "${NESTBOX_HOST_TOKEN_FILE:-}" && -z "$configured_token_file" ]]; then export NESTBOX_HOST_TOKEN_FILE="$TEMP_DIR/host-token"; printf 'test-token-%054d\n' 0 >"$NESTBOX_HOST_TOKEN_FILE"; fi
144
146
 
145
147
  [[ -f "$ROOT/.env" ]] && pass '.env exists' || fail '.env is missing'
146
148
  [[ ! -d "$ROOT/home/.git" ]] && pass 'Page and configuration tree has no embedded Git repository' || fail 'home/.git must not be included in an installation'
@@ -160,6 +162,8 @@ grep -Fq 'deliberately has no Docker socket' "$ROOT/home/configs/opencode/instru
160
162
  grep -Fq 'Commit policy:' "$ROOT/home/configs/opencode/instance.md" && pass 'OpenCode instance policy records commit behavior' || fail 'OpenCode instance policy omits commit behavior'
161
163
  grep -Fq 'Communication language:' "$ROOT/home/configs/opencode/instance.md" && pass 'OpenCode instance policy records communication language' || fail 'OpenCode instance policy omits communication language'
162
164
  grep -Fq '"instance.md"' "$ROOT/home/configs/opencode/opencode.json" && pass 'OpenCode loads instance policy directly' || fail 'OpenCode does not load instance policy'
165
+ grep -Fq '"control"' "$ROOT/home/configs/opencode/opencode.json" && pass 'OpenCode registers the control MCP' || fail 'OpenCode does not register the control MCP'
166
+ [[ -f "$ROOT/docker/control/server.py" && -f "$ROOT/host-runner.mjs" ]] && pass 'Control and host runner sources exist' || fail 'Control or host runner source is missing'
163
167
 
164
168
  if compose config --quiet; then
165
169
  pass 'Compose configuration is valid'
@@ -168,7 +172,7 @@ else
168
172
  fi
169
173
 
170
174
  services="$(compose config --services 2>/dev/null || true)"
171
- for service in nginx php-fpm rollup opencode; do
175
+ for service in nginx php-fpm rollup control opencode; do
172
176
  if grep -qx "$service" <<<"$services"; then
173
177
  pass "Compose defines $service"
174
178
  else
@@ -184,7 +188,7 @@ while IFS= read -r service; do
184
188
  done <<<"$running_services"
185
189
 
186
190
  if ((START)); then
187
- if compose build nginx php-fpm rollup opencode; then
191
+ if compose build nginx php-fpm rollup control opencode; then
188
192
  pass 'All service images build'
189
193
  else
190
194
  fail 'One or more service images failed to build'
@@ -193,14 +197,14 @@ if ((START)); then
193
197
  # The Rollup watcher intentionally writes generated bundles to the bind
194
198
  # mount, so lifecycle mode leaves it untouched and validates its image via
195
199
  # the build plus the bundles served by Nginx.
196
- if compose up -d nginx php-fpm opencode; then
200
+ if compose up -d nginx php-fpm control opencode; then
197
201
  pass 'Application services started'
198
202
  else
199
203
  fail 'Application services failed to start'
200
204
  fi
201
205
  fi
202
206
 
203
- for service in nginx php-fpm opencode; do
207
+ for service in nginx php-fpm control opencode; do
204
208
  if container_running "$service"; then
205
209
  pass "$service is running"
206
210
  else
@@ -217,6 +221,7 @@ fi
217
221
  NGINX_RESTARTS="$(restart_count nginx 2>/dev/null || printf '')"
218
222
  PHP_RESTARTS="$(restart_count php-fpm 2>/dev/null || printf '')"
219
223
  OPENCODE_RESTARTS="$(restart_count opencode 2>/dev/null || printf '')"
224
+ CONTROL_RESTARTS="$(restart_count control 2>/dev/null || printf '')"
220
225
 
221
226
  published="$(compose port nginx 80 2>/dev/null || true)"
222
227
  if [[ "$published" =~ ^\[(.*)\]:([0-9]+)$ ]]; then
@@ -339,6 +344,20 @@ fi
339
344
  status="$(http_request '/_nestbox/publish?topics=nestbox-test:public' '' 'POST' 2>/dev/null || printf '000')"
340
345
  [[ "$status" == "404" ]] && pass 'Publisher endpoint is unavailable on the public port' || fail "Public publisher endpoint returned HTTP $status"
341
346
 
347
+ control_id="$(container_id control)"
348
+ control_binding="$(docker inspect --format '{{json (index .NetworkSettings.Ports "4088/tcp")}}' "$control_id" 2>/dev/null || true)"
349
+ [[ -z "$control_binding" || "$control_binding" == "null" ]] && pass 'Control API is not published on the host' || fail 'Control API is published on the host'
350
+ runner_binding="$(docker inspect --format '{{json (index .NetworkSettings.Ports "4089/tcp")}}' "$control_id" 2>/dev/null || true)"
351
+ if [[ "$runner_binding" == *'"HostIp":"127.0.0.1"'* || "$runner_binding" == *'"HostIp":"::1"'* ]]; then pass 'Host runner API is published on loopback only'; else fail 'Host runner API is not restricted to loopback'; fi
352
+ opencode_id="$(container_id opencode)"
353
+ opencode_mounts="$(docker inspect --format '{{range .Mounts}}{{println .Source .Destination}}{{end}}' "$opencode_id" 2>/dev/null || true)"
354
+ grep -Fq '/var/run/docker.sock' <<<"$opencode_mounts" && fail 'OpenCode receives the Docker socket' || pass 'OpenCode does not receive the Docker socket'
355
+ opencode_env="$(docker inspect --format '{{range .Config.Env}}{{println .}}{{end}}' "$opencode_id" 2>/dev/null || true)"
356
+ if ! grep -Fq '/run/secrets/nestbox-host-token' <<<"$opencode_mounts" && ! grep -Eq '^NESTBOX_HOST_TOKEN=' <<<"$opencode_env"; then pass 'OpenCode cannot read the host runner token'; else fail 'OpenCode can read the host runner token'; fi
357
+ control_mounts="$(docker inspect --format '{{range .Mounts}}{{println .Source .Destination}}{{end}}' "$control_id" 2>/dev/null || true)"
358
+ grep -Fq '/var/run/docker.sock' <<<"$control_mounts" && pass 'Control receives the Docker socket' || fail 'Control does not receive the Docker socket'
359
+ grep -Fq '/run/secrets/nestbox-host-token' <<<"$control_mounts" && pass 'Control receives the host runner token file' || fail 'Control does not receive the host runner token file'
360
+
342
361
  if ((INDEX_PRESENT)); then
343
362
  if compose exec -T php-fpm php -l /home/code/index.php >/dev/null 2>&1; then
344
363
  pass 'Installed super-document passes PHP lint'
@@ -423,7 +442,7 @@ else
423
442
  fail 'EventSource did not receive the expected Nchan event'
424
443
  fi
425
444
 
426
- for entry in "nginx:$NGINX_RESTARTS" "php-fpm:$PHP_RESTARTS" "opencode:$OPENCODE_RESTARTS"; do
445
+ for entry in "nginx:$NGINX_RESTARTS" "php-fpm:$PHP_RESTARTS" "control:$CONTROL_RESTARTS" "opencode:$OPENCODE_RESTARTS"; do
427
446
  service="${entry%%:*}"
428
447
  before="${entry#*:}"
429
448
  after="$(restart_count "$service" 2>/dev/null || printf '')"