@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/.dockerignore +3 -0
- package/.env.example +13 -0
- package/CHANGELOG.md +17 -0
- package/INSTALL.md +25 -5
- package/README.md +10 -10
- package/bin/create-nestbox.js +89 -2
- package/docker/control/Dockerfile +12 -0
- package/docker/control/server.py +660 -0
- package/docker/opencode/Dockerfile +1 -1
- package/docker-compose.yaml +70 -0
- package/home/code/__includes/functions.php +68 -0
- package/home/configs/opencode/control-mcp.js +102 -0
- package/home/configs/opencode/instructions.md +20 -4
- package/home/configs/opencode/opencode.json +11 -0
- package/home/configs/rollup/package-lock.json +10 -13
- package/host-runner.mjs +288 -0
- package/package.host.json +8 -0
- package/package.json +13 -2
- package/scripts/check-package.js +18 -1
- package/template.gitignore +1 -0
- package/tests/control.mjs +70 -0
- package/tests/creator.mjs +43 -0
- package/tests/host-runner.mjs +143 -0
- package/tests/nestbox.ps1 +25 -6
- package/tests/nestbox.sh +24 -5
- package/home/code/scripts-bundle.css +0 -13295
- package/home/code/scripts-bundle.js +0 -6277
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 '')"
|