write_app_files
Write app files into a persistent server-side workspace, then deploy the app; edit later by writing only changed files and redeploying.
Instructions
Write files into an app's server-side workspace, then deploy it.
The two-step idiom for building an app from this conversation:
``write_app_files`` (as many batches as you like) then ``deploy_app``.
The workspace survives the deploy and IS the working copy, so editing is
"write the one changed file → ``deploy_app`` again".
One batch is all-or-nothing: an invalid entry rejects the whole request
and nothing is written. Binary content is refused — deploy binaries from
a repo with ``deploy_git``; assets normally come from the build
(npm/pip). A secret belongs in ``set_secret``, which injects it at launch
without it ever living in a file. WARNING: file contents you pass here
transit this agent's transcript in plaintext; never write secrets into
app files.
SANDBOX: by DEFAULT containers run with all Linux capabilities dropped
(``cap_drop=["ALL"]``) and no-new-privileges — both are operator settings
(``[containers].drop_all_caps`` / ``[containers].no_new_privileges``), so
read the live profile in ``capabilities.sandbox`` rather than assuming: an
operator may have relaxed it. Under that default, an image whose entrypoint
does root work — ``chown``/``chmod`` of its own runtime dirs, dropping
privileges via ``setuid``/``setgid``, writing ``/var/run`` — dies before
your app ever starts. Stock ``nginx`` is the classic trap. Use a non-root
image and a non-root ``USER`` that owns the files it writes: for a static
site, ``nginxinc/nginx-unprivileged`` (listens on **8080**, so set
``port=8080``) or any tiny static server. A crash from this reports
remediation code ``image_needs_privileges`` in ``diagnose_service``. PORT
CONTRACT: the container MUST listen on the declared ``port`` (the tool
argument, else ``nerdit.toml`` ``[deploy].port``, else **8000**) — that is
the only port the daemon publishes and health-checks, so listening anywhere
else fails the health probe with a healthy-looking container. A port below
1024 is NOT the reason a root entrypoint is needed: every container runs on
the docker bridge in its own network namespace (never host networking),
where Docker sets ``net.ipv4.ip_unprivileged_port_start=0``, so binding 80
needs no capability and no root — the chown/setuid work does.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| name | Yes | Workspace name, a DNS label (lowercase a-z0-9 and ``-``, 1-63 chars). A new name creates the workspace; an existing one is edited in place. It is also the app name ``deploy_app`` deploys it under. | |
| files | Yes | Workspace-relative path -> that file's full UTF-8 text; a write replaces the file whole. Paths are relative with no ``..``; ``.git``, ``node_modules``, ``__pycache__``, ``.venv``, ``data/`` and ``*.pyc`` are refused, and ``.env`` / ``.env.*`` by name. Text only (NUL or non-UTF-8 is refused). Caps: 256 KiB per file, 10 MiB and 500 files per workspace. | |
| delete | No | Workspace-relative paths to remove, applied with the writes in the same all-or-nothing batch. Omit (null) to delete nothing; a path that does not exist is a no-op; a path may not be in both ``files`` and ``delete``. | |
| idempotency_key | No | Caller-chosen key: a retry with the same key replays the first result instead of acting twice. Omit and a fresh key is minted per call, so each call acts. |