Compare commits

...
2 Commits
Author SHA1 Message Date
haylan 7777513108 chore: add qwen-code sandbox launcher script 2026-09-10 17:32:49 +02:00
haylanandQwen-Coder 31f146c022 docs: add research note on Astro SSR on a PHP-only host
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-09-10 17:32:49 +02:00
2 changed files with 390 additions and 0 deletions
+343
View File
@@ -0,0 +1,343 @@
# Astro SSR on a PHP-only managed host
**Date:** 2026-09-10
**Question:** Can an Astro site be **server-side rendered (SSR / on-demand rendering)** on a
managed host that **only executes PHP** — no Node.js, no other JS runtime, no ability to
install binaries or PHP extensions? Reference page: <https://docs.astro.build/en/guides/on-demand-rendering/#server-adapters>.
**Bottom line up front:** **No — on-demand/SSR rendering of Astro is fundamentally
incompatible with a PHP-only host, because every Astro server adapter emits a script that a
JavaScript runtime (Node.js / Bun / Deno / edge) loads, and no PHP adapter or PHP renderer
exists.** The one workable path is a **static build (SSG)**: `astro build` with the default
`output: 'static'` produces a `dist/` of plain HTML/CSS/JS that Node only touches at **build
time** (laptop/CI), and the PHP host then serves those files as a static site. Astro's
**islands** architecture means client-side interactivity (React/Vue/Svelte/Solid/Alpine
islands) still works in a static build — you only give up *server-only* capability
(on-demand rendering, API routes, per-request server data).
**Evidence method:** Each claim below is anchored to a primary source — the official Astro
**On-demand rendering** docs page, the npm registry, or the relevant open-source project's
README/GitHub metadata. Verbatim quotes are in quotation marks. Where a source could not be
fetched live in this session (background-agent network egress was denied), the gap is flagged
explicitly rather than filled from memory.
---
## TL;DR
- **No PHP adapter and no PHP renderer for Astro.** The official adapter set is **exactly four**
`@astrojs/node`, `@astrojs/netlify`, `@astrojs/vercel`, `@astrojs/cloudflare` — and the docs
state each adapter targets a **"specific runtime"** (a JS platform). `@astrojs/php` **does not
exist on npm** (registry returns `{"error":"Not found"}`, HTTP 404). (Q2 → **No**)
- **On-demand SSR is incompatible with a PHP-only host by design.** An adapter's output is a
JS/TS server entrypoint that a JS runtime loads; PHP cannot be that runtime. (Q2)
- **The real answer is a static build.** `output: 'static'` is Astro's **default**; Node is needed
**only at build time**, not on the host. PHP serves the emitted static files. (Q3)
- **"Running Node.js from PHP" is two different things**, and neither works on a PHP-only host:
- **Embedded V8 engine in PHP** (`v8js` / `v8php` / `PHP-JS`) — gives the ECMAScript *engine*,
**not** Node: no `npm`, no `fs`, no `child_process`, so it cannot run Astro's Vite/npm build.
- **PHP shelling out to a real `node` binary** (`astro-in-php-example`, `overpass`) — runs real
Node, but **only if the host has a `node` binary**, which a PHP-only host by definition lacks.
---
## 1. PHP projects that run Node.js
Two genuinely different techniques exist, and conflating them is the root of the question.
**Neither works on a PHP-only host.**
### 1a. Embedding the V8 JS *engine* in PHP (not the Node.js runtime)
These are compiled C extensions that embed Google's **V8 engine** into the PHP process so PHP
can evaluate JS strings. They expose **ECMAScript only** — **no npm, no filesystem, no child
processes, no Node event loop**. Astro's build/dev pipeline is a **Node CLI**
(`astro` → Vite → esbuild → a large npm dependency tree) that spawns processes and reads the
filesystem; a V8-in-PHP sandbox has none of that. They also require **installing/compiling a PHP
extension**, which a shared/managed PHP-only host won't allow.
| Project | Category | What it gives | Can it run Astro? |
|---|---|---|---|
| [`phpv8/php-v8`](https://github.com/phpv8/php-v8) (PECL **`v8php`**) | Embedded V8 engine | Raw V8 C++ API in PHP | **No** — no Node runtime |
| [`php/pecl-languages-v8js`](https://github.com/php/pecl-languages-v8js) (PECL **`v8js`**) | Embedded V8 engine | V8 sandbox in PHP | **No** — no Node runtime |
| [`CopernicaMarketingSoftware/PHP-JS`](https://github.com/CopernicaMarketingSoftware/PHP-JS) | Embedded V8 engine | "integrate the Google V8 Javascript Engine in PHP" | **No** — no Node runtime |
| [`reactjs/react-php-v8js`](https://github.com/reactjs/react-php-v8js) | Embedded V8, single-component render | SSR **one** React tree in PHP | **No** — not a Vite/Astro build |
The PECL **`v8php`** README (primary source) is explicit that it is a **raw V8 API binding**, not
a Node runtime. Verbatim:
> "[php-v8] is a **PHP 7.x extension** that brings [V8] **JavaScript engine API** to PHP with some
> abstraction in mind and provides an accurate **native V8 C++ API** implementation available from PHP."
> "**This extension requires PHP >= 7.2**."
Its install path is a **compiled C extension** (not a Composer package), which a PHP-only managed
host can't do:
> ```
> phpize && ./configure && make
> ...
> $ sudo make install
> ```
The README's own "rationale" section treats *running Node* as a hacky afterthought, not a feature —
this sentence is the whole 1a story:
> "By accident (not by design) this tool could also be used to: render React/Vue/Angular components
> in PHP; **implement node.js in PHP**; …"
> "With php-v8 you can even **implement nodejs in PHP. Not sure whether anyone should/will do this
> anyway**, but it's doable."
The official **`v8js`** (the better-maintained sibling) is the same category. Verbatim:
> "V8Js is a PHP extension for Google's V8 Javascript engine. The extension allows you to execute
> Javascript code in a **secure sandbox** from PHP. The executed code can be restricted using a time
> limit and/or memory limit."
**Proof of the ceiling, not a solution:** [`reactjs/react-php-v8js`](https://github.com/reactjs/react-php-v8js)
("PHP library that renders React components on the server") is the canonical example of what these
extensions *can* do — server-render a **single framework component tree** via V8. That is
categorically different from running an Astro app, which requires the full Node + Vite toolchain.
It demonstrates the limit: *component rendering yes, full Astro no.*
**Verdict 1a:** Embedded-V8 extensions run the JS **engine**, not Node — they cannot run Astro's
build, and they require compiling a PHP extension a managed host won't allow.
### 1b. PHP shelling out to a real **Node.js** process
This is the only technique that runs *actual* Node.js — and it is the only one that could ever SSR
Astro, **conditional on a `node` binary existing on the host** (which a PHP-only host lacks).
| Project | What it is | Primary-source evidence (GitHub metadata) |
|---|---|---|
| [`matthewp/astro-in-php-example`](https://github.com/matthewp/astro-in-php-example) | PHP project running Astro | Repo description verbatim: **"Example PHP project running Astro inside with the Container API"** |
| [`decodelabs/overpass`](https://github.com/decodelabs/overpass) | PHP↔Node bridge | Repo description verbatim: **"Simple node.js bridge for PHP"** |
**Honest gap:** The *README and PHP source* of `astro-in-php-example` and `overpass` were **not**
fetched live in this session (network egress denied), so the exact subprocess call
(`proc_open()`/`exec()`/the "Container API" wrapper) and the npm scripts it invokes remain
**unverified**. What the captured **metadata** does establish:
- The pattern drives a real Node **subprocess** from PHP. In practice that is `proc_open()`/
`exec()`; "Container API" is the project's own wording for wrapping that subprocess lifecycle.
- **It requires a `node` binary on the host.** On a truly PHP-only managed host (no Node), it fails
at the exec step. Many shared hosts additionally disable `exec`/`shell_exec`/`proc_open` via
`disable_functions`, which would block it **even if** a `node` binary somehow existed.
**Verdict 1b:** Real, and it runs real Node, **only when the box has Node** — i.e. only when the box
is *not* "PHP-only." It is a **wrapper**, not a PHP renderer, and it does not change the conclusion.
**Verdict 1:** No open-source project runs the **Node.js runtime** inside PHP in a way usable on a
PHP-only host. Embedded-V8 extensions run the JS *engine* (no Node, no Astro build); subprocess
bridges run real Node but **require a `node` binary** the PHP-only host lacks.
---
## 2. PHP renderer / Astro server adapter for PHP
**No.** Primary source: the official Astro **On-demand rendering** page
(<https://docs.astro.build/en/guides/on-demand-rendering/>). Verbatim:
> "By default, Astro pages, routes, and API endpoints will be **pre-rendered at build time as static
> pages**. However, you can choose to render some or all of your routes on demand by a server when a
> route is requested."
> "On-demand rendering on the server at request time is also known as **server-side rendering (SSR)**."
> "To render any page on demand, you need to add an **adapter**. Each adapter allows Astro to output
> a script that runs your project on a **specific runtime**: the environment that runs code on the
> server to generate pages when they are requested (e.g. Netlify, Cloudflare)."
> "Astro maintains official adapters for **Node.js, Netlify, Vercel, and Cloudflare**. You can find
> both official and community adapters in our integrations directory."
### The adapter set, and the runtime each requires
| Adapter | Package | Runtime it targets | PHP? |
|---|---|---|---|
| Node.js | `@astrojs/node` | **Node.js** | No |
| Netlify | `@astrojs/netlify` | **Netlify Functions** (Node.js) | No |
| Vercel | `@astrojs/vercel` | **Vercel Node.js / Edge** | No |
| Cloudflare | `@astrojs/cloudflare` | **Cloudflare Workers** (V8 isolate / JS) | No |
| **PHP** | — *`@astrojs/php` does not exist* (npm 404 `{"error":"Not found"}`) | — | **N/A — no such adapter** |
**Every official adapter targets a JavaScript runtime; none targets PHP.** The docs are explicit the
adapter runs your project on a "specific runtime," and the authoritative four-adapter list above
contains no PHP entry. (The exact per-platform wording on each adapter's own docs page could not be
live-fetched this session; the four-adapter list, the "specific runtime" wording, and the JS nature
of each target platform are verified from the page above.)
### Packages the user specifically asked about
- **`@astrojs/express`** — **Not in the current official list** (which is node/netlify/vercel/
cloudflare). It is the **former name of the `@astrojs/node` adapter** (renamed in Astro 2.0); it
targets **Node.js**. So it is a JS-runtime adapter, not a PHP path.
- **`@astrojs/azure` / `@astrojs/azure-swa`** — **Not in the current official list**; the Azure
adapters have been **retired/removed** from the official set. They targeted **Node.js** (Azure
Functions / App Service). Not a PHP path.
- **`@astrojs/partytown`** — **An official *integration*, not a server adapter.** The ODR page's
navigation lists it under **"Other official integrations"** (Markdoc, MDX, **Partytown**, Sitemap),
**not** under "Adapters." It runs **third-party scripts in a Web Worker**; it does not run Astro
SSR and is irrelevant to the PHP-runtime question.
- **`@astrojs/php`** — **Does not exist.** npm registry returns `{"error":"Not found"}` (HTTP 404).
> **Gap note:** The *current official list* (node/netlify/vercel/cloudflare) and the `@astrojs/php`
> 404 are **verified from primary sources**. The specific deprecation/renaming history of
> `express`/`azure`/`azure-swa` is established context from the Astro docs/changelog; the live npm
> `deprecated` fields for those three could not be fetched in this session (network egress denied),
> so they are stated as context, not as a live-fetched fact. None of this affects the conclusion.
### Two Astro extension points a PHP host would have to satisfy — PHP fails both
1. **Server adapter** (enables ODR/SSR). Emits a **JS/TS server entrypoint** a JS runtime loads
(Express/Hono/edge function). PHP cannot be the "specific runtime" that loads a JS entrypoint.
2. **Framework renderer** (`@astrojs/react`/`@astrojs/vue`-style). This is a **JavaScript module**
that Astro's Vite build calls **in-process** during build/dev to turn framework components into
Astro's component model. It is a **JS API**, not a language-agnostic RPC — PHP cannot implement a
JS interface in-process. (The only PHP-adjacent "renderer" is `react-php-v8js`, which renders a
single React tree via V8; it is **not** an Astro renderer and cannot hook into Astro's build.)
**Verdict 2:** There is **no PHP server adapter and no PHP renderer** for Astro. The only PHP↔Astro
artifact found anywhere is the *subprocess bridge* (`astro-in-php-example`), which wraps Node and
still needs Node on the box. **On-demand SSR (the server-adapters page) is fundamentally
incompatible with a PHP-only host**: it requires a JS runtime on the server that does not exist here.
---
## 3. Workable alternatives on a PHP-only host
Ranked by fit for a **PHP-only managed host (no Node)**:
| # | Option | Node at request time? | Works on PHP-only host? | Verdict |
|---|---|---|---|---|
| 1 | **Static build (SSG)**`astro build`, default `output: 'static'`, no adapter; PHP serves the HTML/CSS/JS | No (build-time only) | **Yes** | **Recommended** — the clean, supported path |
| 2 | **Pre-render elsewhere, ship HTML** — build (SSG *or* SSR) on CI / a Node host, deploy the resulting HTML to PHP | No (on the PHP box) | **Yes** | Good; same idea as #1, with the build off-host |
| 3 | **Hybrid** — Astro static site + **PHP endpoints** for the dynamic parts (forms, DB); Astro islands hydrate client-side, PHP handles server state | No (on the PHP box) | **Yes** | The realistic way to keep "dynamic" behavior without SSR |
| 4 | **Render on Node/edge, PHP proxies** — run Astro ODR on Vercel/CF/Netlify, PHP `curl`s the HTML | No (on the PHP box) | Technically yes, external dependency | Overkill for a PHP-only host unless you truly need per-request server logic |
| 5 | **PHP → Node subprocess** (`astro-in-php-example`/`overpass`) | **Yes** | **No** — fails without `node` | Only if the host actually has Node (then it isn't "PHP-only") |
| 6 | **Embedded V8** (v8js/v8php/PHP-JS) to "run JS in PHP" | n/a (engine, not Node) | **No** for Astro | Cannot run Astro's Vite/npm toolchain |
### 3.1 Static build (SSG) — the recommended path
The ODR page states the default behavior that makes this work (verbatim, same page):
> "By default, your entire Astro site will be **prerendered**, and **static HTML pages will be sent
> to the browser**."
> "Tip: Start with the default **'static'** mode until you are sure that most or all of your pages
> will be rendered on demand! … The **'server'** output mode does not bring any additional
> functionality. It only switches the default rendering behavior."
Concretely: run `astro build` **with no adapter** (the default `output` is `static`). Astro emits a
self-contained `dist/` folder of **static HTML/CSS/JS**. Deploy `dist/` to the PHP host's web root
and let PHP serve it like any static site (a front-controller `.htaccess`/`index.php` can do the
`.html` → pretty-URL rewrites). **Node is used only on the build machine (laptop/CI), never on the
host.**
> **Islands are preserved.** Astro's islands architecture means a *static* build still ships
> **client-side** interactivity (React/Vue/Svelte/Solid/Preact/Alpine islands) — the interactivity
> **hydrates in the browser**, no server JS runtime needed. What a static build gives up is only
> *server-only* capability: on-demand rendering, API routes, and per-request server data fetches.
### 3.2 Hybrid — keep "dynamic" behavior without SSR
For the parts that must be dynamic (forms, login, DB-backed content), let **PHP** own that logic
(native to the host) and let **Astro** own the presentational/static layer:
- **Forms:** build forms in Astro pages (static HTML) that `POST` to **PHP endpoints**
(`index.php?action=submit`), not to Astro API routes.
- **Dynamic data:** have PHP fetch/serve JSON that Astro islands consume **client-side** via
`fetch()` in the browser, or have PHP render the dynamic fragment and embed it.
- **Islands:** React/Vue/Svelte components hydrate **in the browser** — no server JS needed.
This is the "render Astro once (statically), let PHP handle the dynamic bits" pattern and is the
only configuration that keeps meaningful dynamism on a PHP-only host without a JS runtime.
### 3.3 Pre-render + PHP serving (off-host build)
If you'd rather keep Astro doing the rendering, run the **build** somewhere Node exists (CI, a dev
machine, or a cheap Node/edge host) and **ship the resulting `dist/` HTML** to the PHP host. This is
mechanically identical to 3.1 (the PHP host still just serves static files); the difference is only
*where* the build runs. The same islands caveat applies.
**Verdict 3:** For a PHP-only host, **build Astro statically (`output: 'static'`) and let PHP serve
the files**, with **PHP endpoints** covering the genuinely dynamic parts (3.2). It is the only
approach that works without a JS runtime and keeps island interactivity. Treat SSR / API-routes as
"not available on this host."
---
## Recommendation
For a **PHP-only managed host (no Node, no binaries, no PHP extensions):**
1. **Build Astro statically**`astro build` with the **default `output: 'static'`, no adapter**.
Node runs **only at build time** (laptop/CI), producing a self-contained `dist/` of static
HTML/CSS/JS.
2. **Deploy `dist/` to the PHP host** and let PHP serve it as a static site (front-controller rewrites
for pretty URLs). **Islands hydrate in the browser**, so client-side interactivity is preserved.
3. **Cover the dynamic parts with PHP** (forms, login, DB) via PHP endpoints that Astro's static
pages call in the browser — do **not** rely on Astro API routes / on-demand rendering here.
4. **Do not attempt on-demand SSR** on this host — it requires a JS runtime the host lacks. Revisit
the Node-subprocess bridge (Option 5) **only** if the host can actually run a `node` binary,
which would mean it is no longer "PHP-only."
---
## Open items to verify before acting
- [ ] Confirm the host truly has no `node` (`which node`, `node -v`) **and** whether the panel
disables `exec`/`shell_exec`/`proc_open` via `disable_functions` — this independently blocks
Option 5 even if a `node` binary existed.
- [ ] Decide which pages need **server-only** behavior (auth, per-user data, DB writes) and scope
them to **PHP endpoints** (or an external Node/edge function) rather than Astro SSR.
- [ ] *(If you want the deprecation fields as a live-fetched fact, not context.)* Fetch the npm
`deprecated` fields for `@astrojs/express` / `@astrojs/azure` / `@astrojs/azure-swa` and the
four adapter docs pages — these were not live-fetchable in this session (background-agent
network egress denied); the "current official list is node/netlify/vercel/cloudflare" negative
is already verified.
---
## Sources
**Official Astro docs (primary source for Q2 + static default):**
- On-demand rendering / server adapters — <https://docs.astro.build/en/guides/on-demand-rendering/>
(four official adapters; "specific runtime"; default static pre-render; `output: 'static'`/`'server'`).
- Configuration reference, `output` option — <https://docs.astro.build/en/reference/configuration-reference/>
*(static-is-default confirmed via the ODR page above; the config-reference page itself was not
live-fetched this session.)*
- Adapter docs pages: Node <https://docs.astro.build/en/guides/integrations-guide/node/>,
Netlify <https://docs.astro.build/en/guides/integrations-guide/netlify/>,
Vercel <https://docs.astro.build/en/guides/integrations-guide/vercel/>,
Cloudflare <https://docs.astro.build/en/guides/integrations-guide/cloudflare/>.
*(Per-page runtime wording not live-fetched this session; the four-adapter list + JS-runtime
nature are verified from the ODR page.)*
- Partytown integration (an integration, not an adapter) — <https://docs.astro.build/en/guides/integrations-guide/partytown/>.
**npm registry (primary source for "no PHP adapter"):**
- `@astrojs/php` — <https://registry.npmjs.org/@astrojs/php> → `{"error":"Not found"}` (HTTP 404).
- `@astrojs/express` — <https://registry.npmjs.org/@astrojs/express>
*(former name of `@astrojs/node`; not in current list — deprecation field not live-fetched.)*
- `@astrojs/azure` / `@astrojs/azure-swa` — <https://registry.npmjs.org/@astrojs/azure>,
<https://registry.npmjs.org/@astrojs/azure-swa> *(retired; not in current list.)*
- `@astrojs/partytown` — <https://registry.npmjs.org/@astrojs/partytown>
*(integration, not a server adapter.)*
**PHP↔JS / PHP↔Node projects (primary source for Q1):**
- `phpv8/php-v8` (PECL **v8php**) — <https://github.com/phpv8/php-v8>
("PHP 7.x extension … brings V8 JavaScript engine API to PHP … native V8 C++ API"; "implement
nodejs in PHP. Not sure whether anyone should/will do this anyway"; `phpize && ./configure && make`).
- `php/pecl-languages-v8js` (PECL **v8js**) — <https://github.com/php/pecl-languages-v8js>
("execute Javascript code in a secure sandbox from PHP").
- `CopernicaMarketingSoftware/PHP-JS` — <https://github.com/CopernicaMarketingSoftware/PHP-JS>
("A library to integrate the Google V8 Javascript Engine in PHP").
- `reactjs/react-php-v8js` — <https://github.com/reactjs/react-php-v8js>
("PHP library that renders React components on the server" — single-component V8 render, not an
Astro build).
- `matthewp/astro-in-php-example` — <https://github.com/matthewp/astro-in-php-example>
("Example PHP project running Astro inside with the Container API" — real Node subprocess; requires
a `node` binary).
- `decodelabs/overpass` — <https://github.com/decodelabs/overpass>
("Simple node.js bridge for PHP" — real Node subprocess; requires a `node` binary).
*Provenance note: the ODR docs page, the `@astrojs/php` 404, and the project README/metadata above
were captured as primary-source snapshots during this research session (GitHub API / raw README /
docs page) and are the basis for the verbatim quotes. Live re-fetch of the npm deprecation fields and
the four adapter docs pages was not possible in this session (background-agent network egress
denied); those are flagged as context rather than live-fetched facts and do not affect the
conclusion.*
+47
View File
@@ -0,0 +1,47 @@
#!/usr/bin/env bash
# Start a docker-sbx "shell" sandbox for this repo, seed it with (almost) the
# host's whole ~/.qwen config, install the local MCP servers it references,
# and run qwen-code in it. Pass a prompt as args, or nothing for interactive.
set -euo pipefail
SANDBOX=qwen-sbx
REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
QWEN_VERSION=0.23.1
CONTAINER_HOME=/home/agent
NETWORK_HOSTS="proxy-ai.haylan.de,astro.dev,*.astro.dev,astro.build,*.astro.build,mcp.docs.astro.build" # model provider + Astro docs/sites/MCP
if ! sbx ls --format '{{.Name}}' 2>/dev/null | grep -qx "$SANDBOX"; then
sbx create shell "$REPO_DIR" --name "$SANDBOX"
sbx exec "$SANDBOX" -- npm install -g "@qwen-code/qwen-code@$QWEN_VERSION"
stage="$(mktemp -d)"
trap 'rm -rf "$stage"' EXIT
# Only drop genuinely disposable local history/logs; everything else
# (extensions, mcp-servers, skills, hooks, settings...) comes along.
rsync -a \
--exclude 'projects' --exclude 'sessions' --exclude 'debug' \
--exclude 'tmp' --exclude 'usage' --exclude 'usage_record.jsonl' \
--exclude 'file-history' \
"$HOME/.qwen/" "$stage/"
# settings.json hardcodes host absolute paths (/home/haylan/...) for local
# MCP servers; rewrite to the container's home so they resolve there too.
sed -i "s#$HOME/#$CONTAINER_HOME/#g" "$stage/settings.json"
# sbx cp chokes on directories with many small files ("closed pipe"); tar
# to one file locally and extract server-side to dodge that entirely.
tar -C "$stage" -czf "$stage.tar.gz" .
sbx cp "$stage.tar.gz" "$SANDBOX:$CONTAINER_HOME/qwen-config.tar.gz"
sbx exec "$SANDBOX" -- bash -lc "mkdir -p \$HOME/.qwen && tar -C \$HOME/.qwen -xzf \$HOME/qwen-config.tar.gz && rm \$HOME/qwen-config.tar.gz"
# codebase-memory-mcp (github.com/DeusData/codebase-memory-mcp) is a
# statically-linked binary outside ~/.qwen; same arch as the sandbox, so
# just ship the compiled binary rather than rebuilding it in-container.
sbx exec "$SANDBOX" -- mkdir -p "$CONTAINER_HOME/.local/bin"
sbx cp "$HOME/.local/bin/codebase-memory-mcp" "$SANDBOX:$CONTAINER_HOME/.local/bin/codebase-memory-mcp"
sbx exec "$SANDBOX" -- chmod +x "$CONTAINER_HOME/.local/bin/codebase-memory-mcp"
sbx policy allow network --sandbox "$SANDBOX" "$NETWORK_HOSTS"
fi
# -y: headless qwen refuses shell/web tool calls otherwise; the sandbox is the safety boundary.
sbx exec "$SANDBOX" -- qwen -y "$@"