@p10i/create-nestbox 0.1.1 → 0.2.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,13 @@ COMPOSE_PROJECT_NAME=nestbox-change-me
8
8
  # directory, or .. when this installation is <workspace>/.nestbox.
9
9
  WORKSPACE_PATH=.
10
10
 
11
+ # Absolute host-state directory shared only by the native runner and control.
12
+ # The creator generates this value outside the mounted workspace.
13
+ NESTBOX_HOST_QUEUE_PATH=
14
+
15
+ # Override for rootless or nonstandard Docker Engine installations.
16
+ DOCKER_SOCKET_PATH=/var/run/docker.sock
17
+
11
18
  # Use 127.0.0.1 for local access only or 0.0.0.0 for all interfaces.
12
19
  BIND_ADDRESS=127.0.0.1
13
20
 
@@ -26,6 +33,11 @@ NESTBOX_OPENCODE_PUBLIC_URL=
26
33
  APP_UID=1000
27
34
  APP_GID=1000
28
35
 
36
+ # Control logs use standard container output and require no collector. Use text
37
+ # for bounded command output, metadata for lifecycle events only, or off.
38
+ NESTBOX_CONTROL_LOG_OUTPUT=text
39
+ NESTBOX_CONTROL_LOG_MAX_BYTES=262144
40
+
29
41
  # Secrets
30
42
  # OpenCode receives every variable in this file. Keep unrelated secrets out.
31
43
  # Leave the server password empty to disable HTTP basic authentication.
package/CHANGELOG.md ADDED
@@ -0,0 +1,10 @@
1
+ # Changelog
2
+
3
+ ## 0.2.0
4
+
5
+ - Add a private, label-gated container control service and OpenCode MCP adapter.
6
+ - Add native host npm execution through a filesystem bridge with no network listener.
7
+ - Add host-confirmed npm script creation, editing, and deletion.
8
+ - Add reusable PHP control helpers and restrict PHP to self-exec by default.
9
+ - Support both `.nestbox` and direct creator layouts with safe workspace manifest merging.
10
+ - Add bounded structured control logs without introducing an observability dependency.
package/INSTALL.md CHANGED
@@ -12,7 +12,7 @@ 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`, run `npm run host --` from the workspace in a separate terminal, then run from the installation directory:
16
16
 
17
17
  ```sh
18
18
  docker compose config --quiet
@@ -54,7 +54,7 @@ docker version
54
54
  docker compose version
55
55
  ```
56
56
 
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.
57
+ 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
58
 
59
59
  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
60
 
@@ -94,6 +94,15 @@ Copy `.env.example` to `.env` and `home/configs/opencode/instance.example.md` to
94
94
 
95
95
  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
96
 
97
+ Configure the host bridge in the workspace root before Compose starts:
98
+
99
+ - 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.
100
+ - 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.
101
+ - Never overwrite a conflicting script. Stop and ask for a different script name or an explicit migration.
102
+ - Create a private host-state queue outside the workspace, record it in the ignored installation `.runtime/host-runner.json`, and set its absolute path as `NESTBOX_HOST_QUEUE_PATH` in `.env`. The npm creator performs this automatically.
103
+ - Start `npm run host --` before Compose so the queue and heartbeat exist. The runner rejects queue locations inside the mounted workspace.
104
+ - The workspace manifest is mounted read-only in runtime containers. Confirmed changes are written only by the native host runner.
105
+
97
106
  Update the new `home/configs/opencode/instance.md` with:
98
107
 
99
108
  - the communication language;
@@ -143,6 +152,12 @@ docker compose ps
143
152
  docker compose port nginx 80
144
153
  ```
145
154
 
155
+ In a separate terminal, keep the bridge running from the workspace root:
156
+
157
+ ```sh
158
+ npm run host --
159
+ ```
160
+
146
161
  Verify:
147
162
 
148
163
  - `/` retains HTTP 404 and renders the OpenCode implementation form;
@@ -150,6 +165,7 @@ Verify:
150
165
  - `php /sources/php/cli.php __templates/super-document` is rejected;
151
166
  - `/Agent` redirects to OpenCode on the same published port;
152
167
  - the actual OpenCode URL works in the user's client;
168
+ - `npm run test:host` passes and control health reports the host runner as available;
153
169
  - a provider and model are available for agent work, without printing credentials or sending a billable prompt unless the user approves it;
154
170
  - `/_nestbox/health` returns HTTP 200;
155
171
  - a transient PHP event reaches an EventSource subscriber through Nchan;
@@ -227,7 +243,7 @@ Send this work to the internal OpenCode agent. A useful prompt is:
227
243
  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
244
  ```
229
245
 
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.
246
+ 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
247
 
232
248
  ## 10. Git And Generated State
233
249
 
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 bridge exchanges typed files through creator-generated host state outside the container-mounted workspace; it opens no network listener. 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,7 +32,7 @@ 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
 
@@ -1,5 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { spawnSync } from 'node:child_process';
3
+ import { 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
7
  import { dirname, join, resolve } from 'node:path';
@@ -14,12 +16,15 @@ const copyEntries = [
14
16
  'docker',
15
17
  'home',
16
18
  'tests',
19
+ 'host-runner.mjs',
20
+ 'package.host.json',
17
21
  'docker-compose.yaml',
18
22
  '.dockerignore',
19
23
  '.env.example',
20
24
  '.gitattributes',
21
25
  'INSTALL.md',
22
26
  'README.md',
27
+ 'CHANGELOG.md',
23
28
  'LICENSE'
24
29
  ];
25
30
 
@@ -140,6 +145,10 @@ async function configureInstance(installDir, workspaceDir, layout) {
140
145
  errorOnExist: true,
141
146
  force: false
142
147
  });
148
+ const envPath = join(installDir, '.env');
149
+ const workspaceValue = layout === 'nestbox' ? '..' : '.';
150
+ const configuredEnv = (await readFile(envPath, 'utf8')).replace(/^WORKSPACE_PATH=.*$/m, `WORKSPACE_PATH=${workspaceValue}`);
151
+ await writeFile(envPath, configuredEnv, 'utf8');
143
152
 
144
153
  const instanceExample = join(installDir, 'home', 'configs', 'opencode', 'instance.example.md');
145
154
  const instancePath = join(installDir, 'home', 'configs', 'opencode', 'instance.md');
@@ -164,6 +173,70 @@ async function configureInstance(installDir, workspaceDir, layout) {
164
173
  await writeFile(instancePath, instance, 'utf8');
165
174
  }
166
175
 
176
+ function hostScripts(layout) {
177
+ const prefix = layout === 'nestbox' ? '.nestbox/' : '';
178
+ return {
179
+ host: `node ${prefix}host-runner.mjs`,
180
+ 'test:host': `node ${prefix}tests/host-runner.mjs`
181
+ };
182
+ }
183
+
184
+ async function readWorkspacePackage(workspaceDir) {
185
+ const path = join(workspaceDir, 'package.json');
186
+ if (!(await pathExists(path))) return null;
187
+ let document;
188
+ try {
189
+ document = JSON.parse(await readFile(path, 'utf8'));
190
+ } catch (error) {
191
+ throw new Error(`Cannot use workspace package.json: ${error.message}`);
192
+ }
193
+ if (!document || typeof document !== 'object' || Array.isArray(document)) {
194
+ throw new Error('Workspace package.json must contain a JSON object.');
195
+ }
196
+ if (document.scripts !== undefined && (!document.scripts || typeof document.scripts !== 'object' || Array.isArray(document.scripts))) {
197
+ throw new Error('Workspace package.json scripts must be an object.');
198
+ }
199
+ return document;
200
+ }
201
+
202
+ async function preflightHostPackage(workspaceDir, layout) {
203
+ const document = await readWorkspacePackage(workspaceDir);
204
+ if (!document) return;
205
+ for (const [name, command] of Object.entries(hostScripts(layout))) {
206
+ if (document.scripts?.[name] !== undefined && document.scripts[name] !== command) {
207
+ throw new Error(`Refusing to overwrite existing workspace npm script: ${name}`);
208
+ }
209
+ }
210
+ }
211
+
212
+ async function configureHostPackage(installDir, workspaceDir, layout) {
213
+ const scripts = hostScripts(layout);
214
+ const document = await readWorkspacePackage(workspaceDir) || {
215
+ name: 'nestbox-workspace',
216
+ private: true
217
+ };
218
+ document.scripts = { ...(document.scripts || {}), ...scripts };
219
+ await writeFile(join(workspaceDir, 'package.json'), `${JSON.stringify(document, null, 2)}\n`, 'utf8');
220
+ await writeFile(join(installDir, 'package.host.json'), `${JSON.stringify({
221
+ name: 'nestbox-workspace',
222
+ private: true,
223
+ scripts
224
+ }, null, 2)}\n`, 'utf8');
225
+ const stateBase = process.env.NESTBOX_CREATOR_STATE_ROOT
226
+ || process.env.LOCALAPPDATA
227
+ || process.env.XDG_STATE_HOME
228
+ || (process.platform === 'darwin' ? join(homedir(), 'Library', 'Application Support') : join(homedir(), '.local', 'state'));
229
+ const queue = join(stateBase, 'nestbox', 'host-runner', randomUUID());
230
+ await mkdir(queue, { recursive: true });
231
+ const runtime = join(installDir, '.runtime');
232
+ await mkdir(runtime, { recursive: true });
233
+ await writeFile(join(runtime, 'host-runner.json'), `${JSON.stringify({ queue }, null, 2)}\n`, 'utf8');
234
+ const envPath = join(installDir, '.env');
235
+ const composeQueue = queue.replaceAll('\\', '/');
236
+ const configuredEnv = (await readFile(envPath, 'utf8')).replace(/^NESTBOX_HOST_QUEUE_PATH=.*$/m, () => `NESTBOX_HOST_QUEUE_PATH=${composeQueue}`);
237
+ await writeFile(envPath, configuredEnv, 'utf8');
238
+ }
239
+
167
240
  function commandExists(command) {
168
241
  const result = spawnSync(command, ['--version'], { stdio: 'ignore' });
169
242
  return result.status === 0;
@@ -200,9 +273,12 @@ async function main() {
200
273
  throw new Error(`Refusing to overwrite non-empty installation target: ${installDir}`);
201
274
  }
202
275
 
276
+ await preflightHostPackage(workspaceDir, layoutInput);
277
+
203
278
  await mkdir(installDir, { recursive: true });
204
279
  await copyTemplate(installDir);
205
280
  await configureInstance(installDir, workspaceDir, layoutInput);
281
+ await configureHostPackage(installDir, workspaceDir, layoutInput);
206
282
  const gitInitialized = options.git ? initGit(installDir) : false;
207
283
 
208
284
  console.log('\nNestbox project created.');
@@ -210,6 +286,8 @@ async function main() {
210
286
  console.log(`Workspace: ${workspaceDir}`);
211
287
  console.log(`Git initialized: ${gitInitialized ? 'yes' : 'no'}`);
212
288
  console.log('\nNext steps:');
289
+ console.log(` cd ${workspaceDir}`);
290
+ console.log(' npm run host --');
213
291
  console.log(` cd ${installDir}`);
214
292
  console.log(' Edit .env and set COMPOSE_PROJECT_NAME, WEB_PORT, OpenCode credentials, and provider keys.');
215
293
  console.log(' docker compose config --quiet');
@@ -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"]