@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 CHANGED
@@ -15,6 +15,9 @@
15
15
  # npm / Composer caches that may live in the working tree
16
16
  node_modules
17
17
  vendor
18
+ .runtime
19
+ **/__pycache__
20
+ *.pyc
18
21
 
19
22
  # Page state is local by default and must not leak into image layers. A page
20
23
  # that deliberately ships durable data must explicitly revise this policy.
package/.env.example CHANGED
@@ -8,6 +8,14 @@ COMPOSE_PROJECT_NAME=nestbox-change-me
8
8
  # directory, or .. when this installation is <workspace>/.nestbox.
9
9
  WORKSPACE_PATH=.
10
10
 
11
+ # Loopback-only endpoint used by the native host runner. The creator generates
12
+ # a private token; do not expose this port beyond the local host.
13
+ NESTBOX_CONTROL_HOST_PORT=4089
14
+ NESTBOX_HOST_TOKEN_FILE=
15
+
16
+ # Override for rootless or nonstandard Docker Engine installations.
17
+ DOCKER_SOCKET_PATH=/var/run/docker.sock
18
+
11
19
  # Use 127.0.0.1 for local access only or 0.0.0.0 for all interfaces.
12
20
  BIND_ADDRESS=127.0.0.1
13
21
 
@@ -26,6 +34,11 @@ NESTBOX_OPENCODE_PUBLIC_URL=
26
34
  APP_UID=1000
27
35
  APP_GID=1000
28
36
 
37
+ # Control logs use standard container output and require no collector. Use text
38
+ # for bounded command output, metadata for lifecycle events only, or off.
39
+ NESTBOX_CONTROL_LOG_OUTPUT=text
40
+ NESTBOX_CONTROL_LOG_MAX_BYTES=262144
41
+
29
42
  # Secrets
30
43
  # OpenCode receives every variable in this file. Keep unrelated secrets out.
31
44
  # Leave the server password empty to disable HTTP basic authentication.
package/CHANGELOG.md ADDED
@@ -0,0 +1,17 @@
1
+ # Changelog
2
+
3
+ ## 0.3.0
4
+
5
+ - Replace the filesystem host queue with authenticated HTTP long polling managed by `control`.
6
+ - Publish only the dedicated host-runner endpoint on loopback; keep the container API internal.
7
+ - Remove queue paths, bind mounts, polling files, and stale lock/result state.
8
+ - Generate a per-installation runner token file unavailable to application containers and preserve existing MCP and PHP contracts.
9
+
10
+ ## 0.2.0
11
+
12
+ - Add a private, label-gated container control service and OpenCode MCP adapter.
13
+ - Add native host npm execution through a filesystem bridge with no network listener.
14
+ - Add host-confirmed npm script creation, editing, and deletion.
15
+ - Add reusable PHP control helpers and restrict PHP to self-exec by default.
16
+ - Support both `.nestbox` and direct creator layouts with safe workspace manifest merging.
17
+ - Add bounded structured control logs without introducing an observability dependency.
package/INSTALL.md CHANGED
@@ -12,13 +12,15 @@ npm create @p10i/nestbox@latest
12
12
 
13
13
  The published npm package is `@p10i/create-nestbox`; npm maps `npm create @p10i/nestbox` to that package. The creator explains and previews one of the two default layouts before writing files. The recommended layout installs at `<workspace>/.nestbox` so Nestbox stays separate from project files. The direct layout writes Nestbox files into an empty selected directory. The creator copies `.env.example` to `.env`, copies `home/configs/opencode/instance.example.md` to `instance.md`, records npm package provenance in the instance policy, and initializes fresh installation Git history when Git is available.
14
14
 
15
- The creator does not start Docker or collect secrets. After it finishes, edit `.env`, then run from the installation directory:
15
+ The creator does not start Docker, start the host bridge, or collect secrets. Node.js and npm are required for the bridge. After it finishes, edit `.env`, then run from the installation directory:
16
16
 
17
17
  ```sh
18
18
  docker compose config --quiet
19
19
  docker compose up -d --build
20
20
  ```
21
21
 
22
+ After `control` is healthy, run `npm run host --` from the workspace in a separate terminal.
23
+
22
24
  Then run the host test suite documented in section 7.
23
25
 
24
26
  ## 1. Choose Language
@@ -42,7 +44,7 @@ Both defaults keep pages, Compose, Dockerfiles, configuration, dependencies, and
42
44
 
43
45
  Do not present vendoring, a parent-owned engine, a submodule, an external page tree, or another shared-repository arrangement as a default. These are optional adaptations discussed only when the user requests one.
44
46
 
45
- Never overwrite an existing installation. Inspect an existing target and ask whether to update, repair, reuse, or stop.
47
+ Never overwrite an existing installation. Select a different target or stop.
46
48
 
47
49
  ## 3. Check Requirements And Git
48
50
 
@@ -54,7 +56,7 @@ docker version
54
56
  docker compose version
55
57
  ```
56
58
 
57
- Nestbox requires Git, a running Docker Engine, and Docker Compose v2. If something is missing, explain the platform-specific requirement and pause. Do not install system software without approval.
59
+ Nestbox requires Git, Node.js with npm, a running Docker Engine, and Docker Compose v2. If something is missing, explain the platform-specific requirement and pause. Do not install system software without approval.
58
60
 
59
61
  Inspect Git status for the selected directory and installation target before changing files. Preserve unrelated changes. The fresh Nestbox repository remains independent in both default layouts, including when `.nestbox` is nested inside another Git workspace.
60
62
 
@@ -94,6 +96,16 @@ Copy `.env.example` to `.env` and `home/configs/opencode/instance.example.md` to
94
96
 
95
97
  Relative bind paths resolve from the installation directory. The page tree is mounted as `/home/code` in runtime services and as the writable `/nestbox/home/code` agent path. `WORKSPACE_PATH` is mounted as `/workspace` in PHP and OpenCode. `NESTBOX_PAGE_PATH` remains an optional advanced override for a user-requested external page tree.
96
98
 
99
+ Configure the host bridge in the workspace root before Compose starts:
100
+
101
+ - For `.nestbox` layout, create `package.json` from `.nestbox/package.host.json` when absent, or merge only its `host` and `test:host` scripts into the existing manifest.
102
+ - For direct layout, use `node host-runner.mjs` and `node tests/host-runner.mjs` for those scripts. A source clone's publisher-oriented `package.json` may be replaced by this installation manifest after preserving any intentional project metadata.
103
+ - Never overwrite a conflicting script. Stop and ask for a different script name.
104
+ - Set `NESTBOX_HOST_TOKEN_FILE` to a private file outside every container-mounted workspace. The npm creator writes a 64-character token there with owner-only permissions. Only `control` mounts the file; never paste the token into chat or expose it through a page.
105
+ - Confirm that `NESTBOX_CONTROL_HOST_PORT`, normally `4089`, is available. Compose publishes this authenticated runner endpoint on host loopback only.
106
+ - Start `npm run host --` after `control` is healthy. The runner authenticates, long-polls for work, sends heartbeats, and posts results without opening a host listener or sharing queue files.
107
+ - The workspace manifest is mounted read-only in runtime containers. Confirmed changes are written only by the native host runner.
108
+
97
109
  Update the new `home/configs/opencode/instance.md` with:
98
110
 
99
111
  - the communication language;
@@ -109,6 +121,7 @@ Then ask about:
109
121
 
110
122
  - a unique lowercase `COMPOSE_PROJECT_NAME`;
111
123
  - whether to use default port `4180`; verify availability and ask before choosing another port;
124
+ - whether host-runner port `4089` is available; keep it loopback-only;
112
125
  - loopback-only or network access;
113
126
  - OpenCode username and password;
114
127
  - AI providers;
@@ -143,6 +156,12 @@ docker compose ps
143
156
  docker compose port nginx 80
144
157
  ```
145
158
 
159
+ In a separate terminal, keep the bridge running from the workspace root:
160
+
161
+ ```sh
162
+ npm run host --
163
+ ```
164
+
146
165
  Verify:
147
166
 
148
167
  - `/` retains HTTP 404 and renders the OpenCode implementation form;
@@ -150,6 +169,7 @@ Verify:
150
169
  - `php /sources/php/cli.php __templates/super-document` is rejected;
151
170
  - `/Agent` redirects to OpenCode on the same published port;
152
171
  - the actual OpenCode URL works in the user's client;
172
+ - `npm run test:host` passes and control health reports the host runner as available;
153
173
  - a provider and model are available for agent work, without printing credentials or sending a billable prompt unless the user approves it;
154
174
  - `/_nestbox/health` returns HTTP 200;
155
175
  - a transient PHP event reaches an EventSource subscriber through Nchan;
@@ -227,7 +247,7 @@ Send this work to the internal OpenCode agent. A useful prompt is:
227
247
  Extend this Nestbox installation for the following requirement: <request>. Inspect /workspace and /nestbox, follow the internal Nestbox manual, implement and verify all file changes you can, and report the exact host-side activation command for any container or infrastructure change.
228
248
  ```
229
249
 
230
- The internal agent edits pages and infrastructure definitions. It has no Docker socket and must not claim host activation. The external maintainer resumes only to inspect the proposed infrastructure files, run `docker compose config`, apply the narrowest build or recreate operation, and verify health.
250
+ The internal agent edits pages and infrastructure definitions. It has no Docker socket. It may use only the label-gated control MCP for approved container commands and declared host npm scripts; broader activation remains an external-maintainer task.
231
251
 
232
252
  ## 10. Git And Generated State
233
253
 
@@ -255,4 +275,4 @@ When a super-document intentionally changes files in `/workspace`, follow that w
255
275
 
256
276
  ## 11. Optional Adaptations
257
277
 
258
- Retaining upstream Git history, adding an upstream remote, external page trees, parent-owned engines, submodules, vendoring, shared repositories, alternate Compose layouts, and operating-system shortcuts are optional. Explain their ownership and update consequences and obtain explicit confirmation before creating them. They must not obscure the two default installation paths.
278
+ Retaining upstream Git history, adding an upstream remote, external page trees, parent-owned engines, submodules, vendoring, shared repositories, alternate Compose layouts, and operating-system shortcuts are optional. Explain their ownership consequences and obtain explicit confirmation before creating them. They must not obscure the two default installation paths.
package/README.md CHANGED
@@ -6,7 +6,15 @@ Nestbox is an agent-guided workspace for building focused interfaces around proj
6
6
 
7
7
  Its core design is the **super-document**: one cohesive PHP and Markdown document can provide server logic, command-line behavior, forms, HTML, and browser JavaScript. The internal OpenCode agent creates and extends these documents, integrates project runtimes, and prepares infrastructure changes.
8
8
 
9
- Nestbox runs with Docker Compose using Nginx, PHP-FPM, Rollup, and OpenCode. A fresh installation opens with a prompt for creating its first screen instead of shipping a predefined application.
9
+ Nestbox runs with Docker Compose using Nginx, PHP-FPM, Rollup, OpenCode, and a private control service. A fresh installation opens with a prompt for creating its first screen instead of shipping a predefined application.
10
+
11
+ ## Container And Host Control
12
+
13
+ The `control` service is the only container with `/var/run/docker.sock`. Its internal-only API permits exec only when caller, target, project, and root-user labels allow it. OpenCode reaches the API through the bundled control MCP and can target PHP-FPM and Rollup by default. Trusted PHP pages can use `nestbox_control_exec()` and are restricted to PHP-FPM self-exec.
14
+
15
+ Native host commands are limited to scripts already declared in the workspace `package.json`. Keep `npm run host --` running from the workspace when host scripts are needed. The native runner authenticates with an external token file mounted only into `control`, long-polls a loopback-only endpoint, and posts results; no host server or shared filesystem queue is used. MCP can request script additions, edits, or deletions, but the host runner applies them only after the user supplies a short-lived code displayed exclusively in the host terminal.
16
+
17
+ Control emits bounded structured events to ordinary container logs. Use `docker compose logs control`; no telemetry collector, database, or observability dependency is installed. Docker socket access is root-equivalent on the host, and host scripts execute in the trusted writable workspace, so neither mechanism is a sandbox.
10
18
 
11
19
  ## Learn More
12
20
 
@@ -24,18 +32,10 @@ Create a new Nestbox project with npm:
24
32
  npm create @p10i/nestbox@latest
25
33
  ```
26
34
 
27
- The creator previews the exact paths before it writes files. The recommended layout installs Nestbox at `<workspace>/.nestbox` so Nestbox stays separate from your project files. You can also choose a direct install into an empty directory. After creation, edit `.env`, then run the Docker Compose validation and startup commands printed by the creator.
35
+ The creator previews the exact paths before it writes files. The recommended layout installs Nestbox at `<workspace>/.nestbox` so Nestbox stays separate from your project files. You can also choose a direct install into an empty directory. It creates a minimal workspace `package.json` when absent or safely merges non-conflicting `host` and `test:host` scripts into an existing manifest. After creation, edit `.env`, start `npm run host --`, then run the Docker Compose validation and startup commands printed by the creator.
28
36
 
29
37
  For agent-guided installation or repair work, copy and send this to an agent with filesystem, Git, Docker, and terminal access:
30
38
 
31
39
  ```text
32
40
  Install Nestbox by following https://github.com/piontkovsk11andre1/nestbox/blob/main/INSTALL.md completely. Ask all required questions before making changes, preserve unrelated files and Git history, and do not expose secrets.
33
41
  ```
34
-
35
- ## Update
36
-
37
- Run an update request from the workspace containing Nestbox, then copy and send this prompt to an agent with filesystem, Git, Docker, and terminal access:
38
-
39
- ```text
40
- Update this Nestbox installation from https://github.com/piontkovsk11andre1/nestbox. Locate the installation, inspect its local Git history and configuration, read the current remote INSTALL.md and internal Nestbox instructions, preserve pages, instance policy, secrets, volumes, and unrelated changes, explain the proposed update, and ask before destructive or system-level operations. Validate Compose and run the Nestbox host tests after applying the update.
41
- ```
@@ -1,25 +1,35 @@
1
1
  #!/usr/bin/env node
2
2
  import { spawnSync } from 'node:child_process';
3
+ import { randomBytes, randomUUID } from 'node:crypto';
4
+ import { homedir } from 'node:os';
3
5
  import { createInterface } from 'node:readline/promises';
4
6
  import { stdin as input, stdout as output } from 'node:process';
5
- import { dirname, join, resolve } from 'node:path';
7
+ import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
6
8
  import { fileURLToPath } from 'node:url';
7
- import { cp, mkdir, readdir, readFile, stat, writeFile } from 'node:fs/promises';
9
+ import { cp, mkdir, readdir, readFile, realpath, stat, writeFile } from 'node:fs/promises';
8
10
  import { constants } from 'node:fs';
9
11
 
10
12
  const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');
11
13
  const packageJson = JSON.parse(await readFile(join(packageRoot, 'package.json'), 'utf8'));
12
14
 
15
+ function containsPath(root, target) {
16
+ const child = relative(resolve(root), resolve(target));
17
+ return child === '' || (!child.startsWith(`..${sep}`) && child !== '..' && !isAbsolute(child));
18
+ }
19
+
13
20
  const copyEntries = [
14
21
  'docker',
15
22
  'home',
16
23
  'tests',
24
+ 'host-runner.mjs',
25
+ 'package.host.json',
17
26
  'docker-compose.yaml',
18
27
  '.dockerignore',
19
28
  '.env.example',
20
29
  '.gitattributes',
21
30
  'INSTALL.md',
22
31
  'README.md',
32
+ 'CHANGELOG.md',
23
33
  'LICENSE'
24
34
  ];
25
35
 
@@ -140,6 +150,10 @@ async function configureInstance(installDir, workspaceDir, layout) {
140
150
  errorOnExist: true,
141
151
  force: false
142
152
  });
153
+ const envPath = join(installDir, '.env');
154
+ const workspaceValue = layout === 'nestbox' ? '..' : '.';
155
+ const configuredEnv = (await readFile(envPath, 'utf8')).replace(/^WORKSPACE_PATH=.*\r?$/m, `WORKSPACE_PATH=${workspaceValue}`);
156
+ await writeFile(envPath, configuredEnv, 'utf8');
143
157
 
144
158
  const instanceExample = join(installDir, 'home', 'configs', 'opencode', 'instance.example.md');
145
159
  const instancePath = join(installDir, 'home', 'configs', 'opencode', 'instance.md');
@@ -164,6 +178,73 @@ async function configureInstance(installDir, workspaceDir, layout) {
164
178
  await writeFile(instancePath, instance, 'utf8');
165
179
  }
166
180
 
181
+ function hostScripts(layout) {
182
+ const prefix = layout === 'nestbox' ? '.nestbox/' : '';
183
+ return {
184
+ host: `node ${prefix}host-runner.mjs`,
185
+ 'test:host': `node ${prefix}tests/host-runner.mjs`
186
+ };
187
+ }
188
+
189
+ async function readWorkspacePackage(workspaceDir) {
190
+ const path = join(workspaceDir, 'package.json');
191
+ if (!(await pathExists(path))) return null;
192
+ let document;
193
+ try {
194
+ document = JSON.parse(await readFile(path, 'utf8'));
195
+ } catch (error) {
196
+ throw new Error(`Cannot use workspace package.json: ${error.message}`);
197
+ }
198
+ if (!document || typeof document !== 'object' || Array.isArray(document)) {
199
+ throw new Error('Workspace package.json must contain a JSON object.');
200
+ }
201
+ if (document.scripts !== undefined && (!document.scripts || typeof document.scripts !== 'object' || Array.isArray(document.scripts))) {
202
+ throw new Error('Workspace package.json scripts must be an object.');
203
+ }
204
+ return document;
205
+ }
206
+
207
+ async function preflightHostPackage(workspaceDir, layout) {
208
+ const document = await readWorkspacePackage(workspaceDir);
209
+ if (!document) return;
210
+ for (const [name, command] of Object.entries(hostScripts(layout))) {
211
+ if (document.scripts?.[name] !== undefined && document.scripts[name] !== command) {
212
+ throw new Error(`Refusing to overwrite existing workspace npm script: ${name}`);
213
+ }
214
+ }
215
+ }
216
+
217
+ async function configureHostPackage(installDir, workspaceDir, layout) {
218
+ const scripts = hostScripts(layout);
219
+ const document = await readWorkspacePackage(workspaceDir) || {
220
+ name: 'nestbox-workspace',
221
+ private: true
222
+ };
223
+ document.scripts = { ...(document.scripts || {}), ...scripts };
224
+ await writeFile(join(workspaceDir, 'package.json'), `${JSON.stringify(document, null, 2)}\n`, 'utf8');
225
+ await writeFile(join(installDir, 'package.host.json'), `${JSON.stringify({
226
+ name: 'nestbox-workspace',
227
+ private: true,
228
+ scripts
229
+ }, null, 2)}\n`, 'utf8');
230
+ const envPath = join(installDir, '.env');
231
+ const token = randomBytes(32).toString('hex');
232
+ const stateBase = process.env.NESTBOX_CREATOR_STATE_ROOT
233
+ || process.env.LOCALAPPDATA
234
+ || process.env.XDG_STATE_HOME
235
+ || (process.platform === 'darwin' ? join(homedir(), 'Library', 'Application Support') : join(homedir(), '.local', 'state'));
236
+ const tokenDirectory = join(stateBase, 'nestbox', 'host-control');
237
+ await mkdir(tokenDirectory, { recursive: true });
238
+ const tokenPath = join(await realpath(tokenDirectory), `${randomUUID()}.token`);
239
+ const canonicalWorkspace = await realpath(workspaceDir);
240
+ const canonicalInstall = await realpath(installDir);
241
+ if (containsPath(canonicalWorkspace, tokenPath) || containsPath(canonicalInstall, tokenPath)) throw new Error('Host token state directory must be outside the workspace and Nestbox installation.');
242
+ await writeFile(tokenPath, `${token}\n`, { encoding: 'utf8', mode: 0o600 });
243
+ const composeTokenPath = tokenPath.replaceAll('\\', '/');
244
+ const configuredEnv = (await readFile(envPath, 'utf8')).replace(/^NESTBOX_HOST_TOKEN_FILE=.*\r?$/m, () => `NESTBOX_HOST_TOKEN_FILE=${composeTokenPath}`);
245
+ await writeFile(envPath, configuredEnv, 'utf8');
246
+ }
247
+
167
248
  function commandExists(command) {
168
249
  const result = spawnSync(command, ['--version'], { stdio: 'ignore' });
169
250
  return result.status === 0;
@@ -200,9 +281,12 @@ async function main() {
200
281
  throw new Error(`Refusing to overwrite non-empty installation target: ${installDir}`);
201
282
  }
202
283
 
284
+ await preflightHostPackage(workspaceDir, layoutInput);
285
+
203
286
  await mkdir(installDir, { recursive: true });
204
287
  await copyTemplate(installDir);
205
288
  await configureInstance(installDir, workspaceDir, layoutInput);
289
+ await configureHostPackage(installDir, workspaceDir, layoutInput);
206
290
  const gitInitialized = options.git ? initGit(installDir) : false;
207
291
 
208
292
  console.log('\nNestbox project created.');
@@ -214,6 +298,9 @@ async function main() {
214
298
  console.log(' Edit .env and set COMPOSE_PROJECT_NAME, WEB_PORT, OpenCode credentials, and provider keys.');
215
299
  console.log(' docker compose config --quiet');
216
300
  console.log(' docker compose up -d --build');
301
+ console.log(` cd ${workspaceDir}`);
302
+ console.log(' npm run host --');
303
+ console.log(` cd ${installDir}`);
217
304
  console.log(process.platform === 'win32' ? ' .\\tests\\nestbox.ps1' : ' bash tests/nestbox.sh');
218
305
  } finally {
219
306
  rl.close();
@@ -0,0 +1,12 @@
1
+ FROM python:3.12-alpine
2
+
3
+ COPY ./docker/control/server.py /usr/local/bin/nestbox-control
4
+
5
+ RUN python -m py_compile /usr/local/bin/nestbox-control && \
6
+ chmod 0755 /usr/local/bin/nestbox-control
7
+
8
+ WORKDIR /workspace
9
+
10
+ EXPOSE 4088
11
+
12
+ CMD ["/usr/local/bin/nestbox-control"]