| list_gpusA | List GPUs known to the daemon, with live utilization metrics. |
| cluster_statsB | Get aggregate cluster metrics (GPUs in use/total, services up). |
| list_servicesB | List services (apps, models and databases), newest first. |
| get_serviceC | |
| wait_for_serviceA | Use when: a deploy is in flight and you need its real outcome. Blocks until the service converges (healthy) or fails, then returns
``{outcome: converged|failed|timeout|superseded, ...}`` — always, never
raising on a timeout. After ``stop_service``, ``converged`` means the
service is actually stopped.
On ``outcome: failed`` the response also carries ``diagnosis`` — the
FULL ``diagnose_service`` bundle (``remediation.code`` + ``detail``,
crash forensics, health probe, binding readiness and a bounded log
tail), folded in so a failure needs no second call. Branch on
``diagnosis.remediation.code``. It is ``null`` on every non-failed
outcome, and ``null`` on a failure your token is not owner-or-admin for
(the bundle carries app log lines) — in that case call
``diagnose_service`` yourself with a token that owns the app.
|
| service_logsA | Use when: you need the raw output a service or its build printed. ``source`` picks which output you get. **Pass ``source='runtime'`` when
you are debugging a running app** — the build and the app used to share
one stream, so a bare ``grep`` over a deployed app still matches
BuildKit layer chatter (``404B``, ``CACHED``, ``sha256:…``) from the
build that produced it. Lines written by an older daemon are stored as
stdout and therefore read as ``'runtime'`` whichever program printed
them.
``grep`` keeps only lines CONTAINING that text. It is a plain
**literal substring**, never a regex.
All three filters run server-side inside the ``tail`` bound, so ``tail``
counts matching lines: ``grep='Traceback', source='runtime', tail=20``
gives the last 20 app tracebacks out of a million-line log in one
bounded query. Prefer that over pulling a large tail and searching it
yourself.
|
| serveC | Register a service from a prebuilt image. |
| stop_serviceC | Stop a service (desired_state → stopped). |
| restart_serviceB | Restart a service (clears backoff). |
| remove_serviceB | Delete a service (teardown + remove). |
| diagnose_serviceA | Use when: a service is failing and you need the reason, not the logs. Bundles the deploy phase, crash forensics (``last_exit_code``,
``oom_killed``), a fresh health probe, AI-binding readiness, a bounded
log tail and a single ``remediation_code`` to branch on — call it after
``wait_for_service`` returns ``failed`` instead of scraping logs.
Owner-or-admin read: the bundle carries env/secret key *names* and app
log lines, never secret values.
|
| service_statsA | Get live CPU, memory, network and PID usage for a running service. Returns ``{available, stats: {cpu_pct, mem_used_bytes, mem_limit_bytes,
mem_pct, net_rx_bytes, net_tx_bytes, pids}, gpus, cached}``.
**Branch on ``available``, never on zeroes.** ``available: false`` with
``stats: null`` is the normal answer when the service has no running
container or the daemon cannot reach Docker; every individual counter
is likewise ``null`` when unknown, so a missing metric never
masquerades as an idle one. ``cpu_pct`` follows the docker convention —
``100.0`` is one fully-used core, so a multi-core host can exceed 100.
For *why* a service is unhealthy use ``diagnose_service``; this tool
answers *how much* a healthy one is consuming. The daemon caches the
sample for 2 seconds, so polling it in a loop is cheap.
|
| run_commandA | Run a one-off command in a deployed service's image (migrations, seeds). The command runs in a throwaway container built from the service's
current image with the same env, secrets, AI/DB bindings and named
volumes as the live container — the service itself keeps serving.
**Bounded-synchronous**: this call BLOCKS until the command exits or
``timeout_s`` seconds elapse, then returns ``{exit_code, timed_out,
oom_killed, duration_s, run_id, log_tail, ...}``. Branch on
``exit_code``/``timed_out`` — a non-zero exit is a normal 200 result,
not an error. One run at a time per service (409
``service.run_in_progress``).
Because the call blocks for minutes, **supply your own
``idempotency_key`` and REUSE it verbatim if you retry** — that is the
only thing standing between a client-side read timeout and a migration
running twice. A retried key REPLAYS an ``idempotent_replay`` envelope,
NOT the command's output — the run's stdout is deliberately never
cached at rest. That replay is an HTTP 200 carrying ``code:
"idempotent_replay"`` and NO ``exit_code``, so check for that code
before reading ``exit_code``. Re-run under a fresh key if you need the
output again, or read it from ``diagnose_service``'s ``last_run``.
**Key reuse protects a COMPLETED run only.** A failed call un-pins the
key, so a same-key retry really re-executes. In particular ``503
run.interrupted`` means the container started and was lost — the
command may have partially applied — and retrying it, with any key,
runs it again: stop and inspect instead. And if the daemon died
mid-run, the key stays claimed for 24 h and the retry returns ``409
idempotency_in_progress``; mint a fresh key once you have confirmed
the run is over.
|
| serve_modelA | Serve a local AI model as an OpenAI-compatible endpoint. Serving is asynchronous: the call returns immediately with the
model row in ``building`` while the daemon pulls the image and weights
off-tick; poll ``list_models`` (or ``get_service``) until it is
``running``. Lifecycle (stop / restart / delete) goes through the
existing service tools (``stop_service`` / ``restart_service`` /
``remove_service``).
An app connects to this model by declaring an ``[ai.<name>]`` binding
(provider ``ollama``, model = this model); at each launch it receives
``NERDIT_AI_<NAME>_URL/_KEY/_MODEL``, and the ``default`` binding also
sets ``OPENAI_BASE_URL/OPENAI_API_KEY/OPENAI_MODEL`` so an unmodified
OpenAI SDK reaches this endpoint with no code change.
|
| list_modelsA | List served AI models (kind=model workloads), bounded to limit per page. Returns a cursor-paginated page; pass ``cursor`` from the previous page
to continue. A newly served model shows ``building`` until its off-tick
pull completes; lifecycle actions go through the service tools.
|
| create_databaseA | Provision a managed database (kind=database workload). Provisioning is asynchronous: the call returns immediately with the
database row in ``building`` while the daemon pulls the image off-tick
and launches the server; poll ``list_databases`` (or ``get_service``)
until ``db_ready`` is true. Lifecycle (stop / restart / delete) goes
through the existing service tools.
The daemon mints the password server-side and stores it write-only — it
is NEVER returned by this tool (key names only). An app connects to this
database by declaring a ``[db.<name>]`` binding (``provider="managed"``,
``database`` = this database's name — the ``database`` field is
required, there is no implicit default); at each launch it receives
``NERDIT_DB_<NAME>_URL``, and the ``default`` binding also sets
``DATABASE_URL`` (Postgres) or ``REDIS_URL`` (Redis) with the composed
DSN so an unmodified client library connects with no code change.
|
| list_databasesA | List managed databases (kind=database workloads), bounded to limit per page. Returns a cursor-paginated page. A newly created database shows
``building`` until its off-tick image pull and readiness probe complete
(``db_ready`` true).
The ``endpoint`` is password-free by construction; lifecycle actions go
through the service tools.
|
| dump_databaseA | Capture a logical, application-consistent dump of a managed database. The daemon runs the engine's own dump tool — ``pg_dump --format=custom``
for Postgres, ``redis-cli --rdb`` for Redis — from a throwaway container
built from the database's OWN image, against the running server. This is
the dump to take before a migration or a destructive change: unlike the
volume backup (``nerdit backup --volume``, no tool), it is consistent by
the engine's rules rather than by the filesystem's.
CUSTODY: the artifact is a server-side tar under the daemon's backups
directory holding **every row** of ``name``; it is never served over
HTTP and nothing but metadata comes back here — basename, size, sha256,
engine, format, duration. Treat the basename as the handle: it is what
``nerdit db restore`` takes.
RETENTION: the daemon keeps the last ``[retention].dump_keep_last``
dumps per database (default 5) and prunes older ones on its hourly
sweep, so a dump is not an archive. Copy one off the box if it must
outlive that window.
TIMEOUT: the default matches the usual MCP client read budget, not
the route's own 900: if your client gives up first, **a longer dump still completes
server-side and appears in ``list_database_dumps``**. So a read timeout
here is not a failure — call ``list_database_dumps`` and take the newest
entry. Do NOT blind-retry: this tool mints its own idempotency key per
call, so a retry is a second real dump (and the first one may still be
running, which answers ``409 dump.in_progress``).
Authorization is **owner-or-admin plus token scope**: a scoped submitter
can dump the databases it created, which is what makes this usable over
the (submitter-capped) cloud link. Refusals you should branch on: ``409
dump.database_not_ready`` (not running, or still doing initdb — poll
``list_databases`` for ``db_ready``), ``409 dump.in_progress`` (this
database already has one running; distinct from the daemon-wide
``backup.in_progress``), ``409 dump.too_many_in_flight``, ``409
dump.insufficient_disk`` (the error object carries top-level
``required_bytes`` / ``free_bytes``), ``422 dump.not_a_database``, and
``500 dump.failed``
whose ``hint`` is the tool's own last output line. There is deliberately
**no restore tool** — restoring replaces live data and stays on the CLI
(``nerdit db restore``) and REST.
|
| list_database_dumpsA | List the dump tars this daemon holds for one managed database. Newest first, ``{dump, size_bytes, created_at}`` per row, where
``created_at`` is the tar's mtime (the listing never opens an archive).
Metadata only, like ``dump_database``: the tars themselves stay under
the daemon's backups directory and are never served over HTTP, and each
one holds every row of ``name``.
Two things this read is for. It is the recovery path when a
``dump_database`` call outran your client's read budget — **the dump
still completes server-side and shows up here**, so list before you
retry. And it is how you check retention: the daemon keeps the last
``[retention].dump_keep_last`` per database (default 5) and prunes the
rest hourly, so on a default daemon what is listed here is all there is.
Retention can be turned off (``dump_keep_last = 0``), so the listing is
capped at the newest 500 instead: ``truncated`` is true when older tars
exist on the box that this page does not name.
Owner-or-admin plus token scope — the same gate as the dump that
produced them, because knowing which dumps a database has is knowing
something about the data.
|
| deployA | Use when: the source is a local folder. Asynchronous — follow with wait_for_service. Deploys that folder (build + run + URL), or rolls back with
``rollback=True``. The response carries ``next_step`` — a machine-shaped
``{tool, args, why}`` naming the very call to make next — because a 201
here means "the build was accepted", never "the app is up".
``path`` is read by the process serving this tool, so it works only on
the local ``nerdit mcp`` process; on a remote/HTTP connection it is
refused with ``mcp.local_path_unavailable``. Use ``write_app_files`` +
``deploy_app`` there, or ``deploy_git`` / ``deploy_template``.
``rollback=True`` needs no path and works over either transport.
If the app's ``nerdit.toml`` declares ``[ai.*]`` bindings, each is
injected at launch as ``NERDIT_AI_<NAME>_URL/_KEY/_MODEL``, and the
``default`` binding also sets ``OPENAI_BASE_URL/OPENAI_API_KEY/
OPENAI_MODEL`` — point the app's OpenAI SDK at these; no code change
needed. ``dry_run=True``
returns the build/config plan diff (env key names only, values never).
``nerdit.toml`` ``[deploy]`` keys — anything else is IGNORED and
reported back in the response ``hints``: ``name``, ``port``, ``gpus``,
``start``, ``health``, ``health_type`` (``http``|``tcp``),
``memory_limit``, ``cpu_limit``, ``volumes``, ``release``, ``cutover``,
``auto_deploy``, ``edge_auth``, ``build_settings``. Generated Node builds run
``scripts.build`` automatically;
override it through ``[deploy.build_settings]`` or use a Dockerfile.
``release`` runs once inside the new image before traffic moves; if it
fails the deploy settles ``failed`` and the image reverts, never the data —
whatever the migration already committed stays, so write idempotent
migrations. ``cutover`` null = zero-downtime swap
when eligible, false = same-port swap.
On a successful non-dry-run deploy the
response carries ``summary`` (app/status/version/public_url), ``hints``
(ordered one-liners, never empty) and ``next_step`` (the structured
follow-up call); a ``dry_run`` plan and a ``rollback`` response carry
none of the three — read a plan's advisories from its ``warnings``. On a
node in ``path`` proxy mode the app is served at ``/<name>/``, so a
frontend must be built with that base
path; in ``subdomain`` mode it is served at the root of its own
hostname and needs none — read ``capabilities.proxy.mode``.
``build_settings`` overrides
preset/install/build/start/node_version/package_manager/subdir/public_env.
``preset`` selects node/nextjs/python/dockerfile; null resets to
repository/default detection.
An existing Dockerfile retains precedence.
Omit it to preserve saved overrides; a null field resets to repository/default,
and ``build: false`` skips compilation.
``public_env`` passes public build-time variables (shown in clear in a dry
run; secret references are refused). Secret mounts are unsupported;
runtime env is unchanged.
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.
|
| deploy_gitA | Use when: the app lives in a Git repo. Asynchronous — follow with wait_for_service. Clones ``repo_url`` server-side (public GitHub by default), then builds
and runs it like ``deploy``. The token reference is resolved server-side
— a raw credential never passes through the tool.
``dry_run=True`` returns the build/config plan diff (env key names only,
values never).
If the repo's ``nerdit.toml`` declares ``[ai.*]`` bindings, each is
injected at launch as ``NERDIT_AI_<NAME>_URL/_KEY/_MODEL``, and the
``default`` binding also sets ``OPENAI_BASE_URL/OPENAI_API_KEY/
OPENAI_MODEL`` — point the app's OpenAI SDK at these; no code change
needed.
``nerdit.toml`` ``[deploy]`` keys — anything else is IGNORED and
reported back in the response ``hints``: ``name``, ``port``, ``gpus``,
``start``, ``health``, ``health_type`` (``http``|``tcp``),
``memory_limit``, ``cpu_limit``, ``volumes``, ``release``, ``cutover``,
``auto_deploy``, ``edge_auth``, ``build_settings``. Generated Node builds run
``scripts.build`` automatically;
override it through ``[deploy.build_settings]`` or use a Dockerfile.
``release`` runs once inside the new image before traffic moves; if it
fails the deploy settles ``failed`` and the image reverts, never the data —
whatever the migration already committed stays, so write idempotent
migrations. ``cutover`` null = zero-downtime swap
when eligible, false = same-port swap.
On a successful non-dry-run deploy the
response carries ``summary`` (app/status/version/public_url), ``hints``
(ordered one-liners, never empty) and ``next_step`` (the structured
follow-up call); a ``dry_run`` plan carries none of the three — read a
plan's advisories from its ``warnings``. On a node in ``path``
proxy mode the app is served at ``/<name>/``, so a frontend must be
built with that base path.
``build_settings`` overrides
preset/install/build/start/node_version/package_manager/subdir/public_env.
``preset`` selects node/nextjs/python/dockerfile; null resets to
repository/default detection.
An existing Dockerfile retains precedence.
Omit it to preserve saved overrides; a null field resets to repository/default,
and ``build: false`` skips compilation.
``public_env`` passes public build-time variables (shown in clear in a dry
run; secret references are refused). Secret mounts are unsupported;
runtime env is unchanged.
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.
|
| redeploy_serviceA | Use when: a git app needs a new commit. Asynchronous — follow with wait_for_service. Re-clones the repository and branch/tag the service was deployed from
and rebuilds it, so picking up a new commit is one call with the service
name alone. Use this instead of ``deploy_git`` when the service already
exists — every coordinate (repo, ref, subdir, private-repo credential
reference) is read back off the service. A service deployed from a
folder/zip has no recorded source and is refused. ``dry_run=True`` returns
the build/config plan diff (env key names only, values never). A
non-dry-run response carries
``summary``, ``hints`` (never empty) and ``next_step`` — the structured
follow-up call.
|
| list_app_templatesA | List the app template store — deployable starter apps (id/name/coordinates). Deploy one with ``deploy_template``. These are full app templates
(web/API/AI starters), not per-service presets.
|
| deploy_templateA | Use when: you want a starter app. Asynchronous — follow with wait_for_service. Resolves ``template_id`` (see ``list_app_templates``), clones its
catalog repo server-side, and deploys it under ``name``. ``env`` and the
deploy overrides layer over the template's defaults.
The response carries ``summary``, ``hints``
(never empty) and ``next_step`` — the structured follow-up call — since
a 201 here means the build was accepted, not that the app is up.
An existing Dockerfile retains precedence over ``build_settings``.
Omission preserves overrides; a null field resets to repository/default.
``dry_run=True`` previews without writes,
building or storing secrets. ``public_env`` passes public build-time
variables (shown in clear in a dry run; secret references are refused);
secret mounts are unsupported.
If the template declares ``[ai.*]`` bindings, each is injected at launch
as ``NERDIT_AI_<NAME>_URL/_KEY/_MODEL``, and the ``default`` binding also
sets ``OPENAI_BASE_URL/OPENAI_API_KEY/OPENAI_MODEL`` — point the app's
OpenAI SDK at these; no code change needed. WARNING: the ``secrets`` values you pass
here transit this agent's transcript in plaintext; for high-value keys
prefer ``nerdit secrets set`` from the CLI and deploy without ``secrets``.
|
| write_app_filesA | 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.
|
| list_app_filesA | List an app's workspace: paths, sizes, sha256 and totals. What this lists is exactly what ``deploy_app`` would build — the exclude
rules are enforced at write time, not at deploy time. Bounded at 500
files by construction, so there is no paging.
|
| read_app_fileA | Read one file back from an app's workspace as text. Use it before patching a file you did not write in this conversation, so
the next ``write_app_files`` replaces content you have actually seen.
|
| deploy_appA | Use when: you used write_app_files. Asynchronous — follow with wait_for_service. Distinct from ``deploy`` (which uploads a folder from the machine the MCP
server runs on) and from ``deploy_git`` (which clones a repository): this
one deploys the workspace of the same name, so it is the deploy verb for
an app you wrote in this conversation. Call it again after each
``write_app_files`` to ship an edit — ``redeploy_service`` refuses
workspace apps by design and points back here.
If the workspace's ``nerdit.toml`` declares ``[ai.*]`` bindings, each is
injected at launch as ``NERDIT_AI_<NAME>_URL/_KEY/_MODEL``, and the
``default`` binding also sets ``OPENAI_BASE_URL/OPENAI_API_KEY/
OPENAI_MODEL``. A dry run returns the build/config plan diff with env key
names only, values never, and mints no idempotency key.
``nerdit.toml`` ``[deploy]`` keys — anything else is IGNORED and
reported back in the response ``hints``: ``name``, ``port``, ``gpus``,
``start``, ``health``, ``health_type`` (``http``|``tcp``),
``memory_limit``, ``cpu_limit``, ``volumes``, ``release``, ``cutover``,
``auto_deploy``, ``edge_auth``, ``build_settings``. Use ``[deploy.build_settings]``
for build overrides, or a Dockerfile for custom builds.
``release`` runs once inside the new image before traffic moves; if it
fails the deploy settles ``failed`` and the image reverts, never the data —
whatever the migration already committed stays, so write idempotent
migrations. ``cutover`` null = zero-downtime swap
when eligible, false = same-port swap. ``env`` values are literal and
never resolve ``${secrets.*}``: a secret reaches the app through
``set_secret`` / ``nerdit secrets set`` (injected at launch, overriding
an ``env`` key of the same name).
On a successful non-dry-run deploy the
response carries ``summary`` (app/status/version/public_url), ``hints``
(ordered one-liners, never empty) and ``next_step`` (the structured
follow-up call); a ``dry_run`` plan carries none of the three — read a
plan's advisories from its ``warnings``. On a node in ``path``
proxy mode the app is served at ``/<name>/``, so a frontend must be
built with that base path.
``build_settings`` overrides
preset/install/build/start/node_version/package_manager/subdir/public_env.
``preset`` selects node/nextjs/python/dockerfile; null resets to
repository/default detection.
An existing Dockerfile retains precedence.
Omit it to preserve saved overrides; a null field resets to repository/default,
and ``build: false`` skips compilation. Commands run inside the build container.
``public_env`` passes public build-time variables (shown in clear in a dry
run; secret references are refused). Secret mounts are unsupported;
runtime env is unchanged.
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.
|
| share_serviceA | Use when: you need a URL you can open (public_url is LAN-only). Exposes the app at ``https://<name>--<slug>.<domain>/`` via the cloud
link. ``access='private'`` (default) opens only for the owner of this
machine's Nerdit account, signed in to the Nerdit App, and the people they
allow there — works on every linked node, nothing to configure.
``access='public'`` makes the URL world-reachable: it needs an
account in good standing (free during the public beta; else 409
``share.not_entitled``) AND either a
``[deploy].edge_auth`` on the app or ``consent=true`` (else 409
``share.unprotected``) — public means public. The node must be linked
(409 ``share.link_required``). Returns ``{access, url, state}``; ``state``
is ``ready`` | ``link_down`` | ``not_entitled``. The same URL then appears
in ``get_service`` / ``list_routes`` under ``public_urls`` (kind
``hosted``).
``state`` and ``origin`` answer two DIFFERENT questions and you need
both. ``state`` is about the LINK — is the hosted path provisioned.
``origin`` is about the APP behind it: ``{status, answers, hint}``, where
``answers`` is true only while the service is running. A share can be
``state: ready`` with ``origin.answers: false``, and then the URL returns
``share.not_shared`` (the hosted resolver refuses a service with no live
origin) — which reads exactly like "never shared". When ``answers`` is
false, ``origin.hint`` says what to do; call ``diagnose_service``.
|
| unshare_serviceA | Use when: an app should stop being reachable at its hosted URL. The URL stops answering on the next request (the tunnel re-reads the row
per stream — there is no cache to wait out). Idempotent: removing a
share that does not exist is a success returning ``removed: false``,
never an error. The response is the removal receipt
(``{service_name, removed}``), not a share view — read ``share_service``
or ``get_service`` for the state of a share that still exists.
|
| add_domainA | Serve a deployed app at a domain YOU own, from this node's own proxy (P26). Unlike ``share_service`` (Nerdit's domain, over the cloud tunnel), this
is the node's embedded Caddy answering ``https://<domain>/`` directly —
in either proxy mode, at the ROOT, so a frontend needs no ``/<app>/``
base path. The one thing the daemon cannot do for you: point the
domain's DNS (A/AAAA/CNAME) at this machine. By default the domain is
served with the node's internal CA, so a client must run
``nerdit trust`` once. ``acme=True`` requests a public ACME
certificate; it needs an ADMIN token (403 ``domain.acme_forbidden``,
``reason: acme_admin_only`` — the node has one ACME account whose rate
limits every domain on it shares) and 409 ``domain.acme_disabled``
unless the daemon has ``[proxy.acme].enabled``.
Refusals:
422 ``domain.invalid`` — the name broke exactly one grammar rule and the
message names it (``empty``, ``whitespace``, ``not_bare``,
``has_port``, ``wildcard``, ``ip_literal``, ``idn``, ``single_label``,
``grammar``, ``reserved``); 409 ``domain.taken`` (one domain, one
service — never silently re-pointed); 422 ``domain.kind_unsupported``
(models and databases are not routed).
Returns ``{domain, url, state, cert_state, created}``; ``state`` is
``ready`` | ``withheld`` (``withheld`` = the proxy is off, the app is
not routed yet, or its ``[deploy].edge_auth`` secret does not resolve —
the row is stored either way and answers once it is). ``cert_state`` is
the orthogonal certificate fact: ``internal`` (this node's CA) |
``disabled`` (``acme`` asked for, ``[proxy.acme]`` off) | ``pending``
(issuing, or failing — check ``doctor``) | ``issued`` | ``expired``.
The URL then appears in ``get_service`` / ``list_routes`` under
``public_urls`` (kind ``domain``).
|
| remove_domainA | Remove a custom domain; its route disappears on the next reconcile tick. Idempotent. |
| list_hooksA | List a deployed app's inbound webhooks (owner or admin only). Returns ``{service_name, hooks: [{name, sender, path, header, prefix,
secret_ref, secret_key, url, created_at}]}``. ``url`` is null until the
machine is linked, and answers only once the app is shared.
|
| add_hookA | Give a deployed app an inbound webhook address a provider can POST to. The machine verifies each delivery's signature before the app sees it,
then POSTs the provider's headers and exact body to ``path``. No secret
travels through this tool: the response carries ``url`` (paste it at the
provider) and ``secret_key``; then store the provider's signing secret
with ``set_secret(name, {secret_key: value})``. ``url`` is null until
the machine is linked, and answers only once the app is shared.
Refusals: 422 ``hook.invalid`` (``reason`` ``name`` or ``shape``), 422
``hook.kind_unsupported`` (models and databases), 409 ``hook.exists``
(a different webhook already has that name). A replay of the same
webhook answers ``created: false``.
|
| remove_hookA | Remove an inbound webhook; its address stops answering. Idempotent. The signing secret stays set (remove it with ``remove_secret``). A
webhook still declared in nerdit.toml comes back with a NEW address on
the next deploy.
|
| create_projectA | Create an empty project, reserving its name for your token. A project groups one app's services, the models and databases they
reference, and their public addresses. The name is reserved for the
token that created it: another token's deploy, ``serve_model`` or
``create_database`` of that name is refused with 409 ``project.owned``
(an admin bypasses), and the project remains after ``remove_service``
until ``delete_project`` releases it. You rarely need this before a
deploy — every deployed app already becomes a project of its own name —
but it lets you hold a name ahead of the first deploy. A name another
token already reserved through ``set_secret`` is 409
``service.name_claimed``; a name in use is 409 ``project.exists``.
|
| list_projectsA | List the projects this token can see, bounded to limit per page. Each entry carries the project's services (state, endpoint) and every
advertised address. A scoped token sees only the projects its scope
names. Cursor-paginated; use ``get_project`` for the full picture of one.
|
| get_projectA | The one-call picture of a project: read this before you deploy or diagnose. Returns the project's services with their state, last deploy and
endpoint; the models and databases they reference with a readiness
flag (``null`` when readiness cannot be judged without a secret); every
public address under ``addresses``; and ``home``, the node it lives on
(always this node today). A name outside your scope is 403; an unknown
name is 404. Secret values never appear here.
|
| delete_projectA | Delete a project: every service in it, then the row, releasing the name. Services go one by one through the same cascade as ``remove_service``
(owner-or-admin each; a dependency guard or an in-flight run refuses a
service exactly as there). The project row goes last and only when
every service went: a refusal leaves the project in place with 409
``project.delete_incomplete`` listing ``deleted`` and ``failed``, and
re-running after the fix is idempotent. Owner or admin only.
|
| set_variableA | Set/merge a project's variables; write-only unless secret=false. Returns names only. WARNING: the values you pass here transit this agent's transcript in
plaintext, whether ``secret`` is omitted, ``true`` or ``false``. For any
credential, do not pass the value at all: ask the human to run
``nerdit vars set <project> --secret --prompt KEY`` (hidden input) and
only ever name ``KEY`` yourself. Use this tool for values that are fine
to appear in a transcript (a log level, a feature flag, a public URL).
A variable reaches a service's environment at launch. Precedence, lowest
to highest: the manifest ``env`` table, the project scope, the service
scope, then platform-injected bindings (``OPENAI_BASE_URL``,
``DATABASE_URL``…), which a variable can never override. Setting a key
again flips its plain/secret flag in that scope. Only the project's owner
or an admin may write (another token gets 403).
A running service picks the change up on its next restart or redeploy.
The machine-wide scope is not reachable here: use ``set_secret`` with
``shared``.
|
| resolve_variablesA | What a launch would see: per key, the winning scope and whether it is plain. No values. The loop is "the agent asks, the human supplies". Call this before a
deploy to see which keys the service will receive (``scope`` is
``project`` or ``production/<service>``). When a key the app needs is
missing, do NOT invent or request the value in chat: ask the human to
run ``nerdit vars set <project> --secret --prompt KEY``, then call this
again to confirm the key is present. A secret value is never returned by
any tool. Owner or admin only; a name outside your scope is 403.
|
| write_project_filesA | Write a project's source tree (its nerdit.toml included), then apply_project. Exactly ``write_app_files`` on the workspace named after the project:
same all-or-nothing batch, same text-only rule, same caps, and
``list_app_files`` / ``read_app_file`` read it back under the project
name. One tree holds EVERY service; a service built from a subfolder
says so with ``build_settings.subdir``. The root ``nerdit.toml`` is the
declaration::
[project]
name = "asso"
[services.web]
port = 3000
[services.api]
port = 8000
build_settings = { subdir = "apps/api" }
[vars]
required = ["API_KEY"]
``[services.<name>]`` takes the ``[deploy]`` keys except ``name``: the
service ``web`` is served under the project name (``asso``), any other
under ``<service>--<project>`` (``api--asso``), and that label is the
name every other tool takes. ``[ai.*]`` / ``[db.*]`` bindings stay
top-level tables shared by every service; a declaration cannot create
a database (``create_database`` first, then bind it). WARNING: file
contents you pass here transit this agent's transcript in plaintext;
never write secrets into project files.
|
| apply_projectA | Apply a project's declaration: deploy every [services.<name>] of its nerdit.toml. The loop: ``get_project`` (read) → ``apply_project(dry_run=true)`` →
if ``status`` is ``waiting_for_variables``, do NOT invent or request
the values in chat: ask the human to run ``nerdit vars set <project>
--secret --prompt KEY`` for each name in ``missing``, then dry-run
again → ``apply_project`` → ``wait_for_service`` once per ``label`` in
``services`` → ``diagnose_service`` on any that failed.
The response lists ``services`` in declaration order, each with its
``label`` (the name every other tool takes: ``web`` is the project
name, any other service ``<service>--<project>``), ``action``
(``fresh``/``redeploy``) and, on a dry run, its ``plan`` (env key names
only, values never); ``public_urls`` carries every advertised address.
``waiting_for_variables`` deploys nothing and returns names only.
Asynchronous like every deploy: ``applied`` means accepted, not healthy.
Every refusal lands before the first build, and a dry run creates
nothing and mints no idempotency key. A project another token owns is
409 ``project.owned``. There is NO rollback across services: a failure
on one is ``project.apply_incomplete``, whose ``services`` lists the
ones already applied and ``failed`` the one that was not — fix it and
apply again (done services redeploy, the rest are retried). A
``[deploy]``-only ``nerdit.toml`` is not a declaration: use
``deploy_app`` / ``deploy_git`` for it (they answer 422
``deploy.use_apply`` on a declaration).
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.
|
| project_logsA | Read bounded logs of one service resolved within an explicit project. Prefer the immutable project ID returned by list_projects. Membership is
resolved by the daemon; a same-name replacement cannot inherit a retired ID.
Existing node grants and local token roles still apply.
|
| diagnose_projectA | Diagnose one service of an explicit project under the existing owner/admin policy. Prefer an immutable project ID. The daemon resolves project membership and
reuses diagnose_service's failure bundle; this grants no additional authority.
|
| set_secretA | Set/merge write-only secrets for a service; returns key names only. WARNING: the values you pass here transit this agent's transcript in
plaintext; for high-value shared keys (broadly-scoped API keys,
credentials shared across every app) prefer ``nerdit secrets set --shared`` from the CLI
instead of routing them through an agent. The leak-free path for any
value: a human runs ``nerdit secrets set <service> --prompt KEY``
(hidden input) and you only ever name ``KEY``; it is injected at
launch, overriding an ``env`` key of the same name. You may set secrets
before the service exists; the name is then reserved for your token
until you deploy it (another token's deploy of that name gets 409
``service.name_claimed``).
|
| list_secret_namesA | List a service's secret key names (values are never returned). |
| remove_secretA | Delete one secret key, or all of a service's secrets when key is omitted. Deleting a shared key can break every app still referencing it via
``${secrets.shared.KEY}``.
|
| list_ai_routesA | List the machine AI gateway's aliases and whether the gateway is enabled. Each alias shows its provider, provider model, base_url and the
``${secrets.shared.KEY}`` reference of its key: never a key value.
|
| list_ai_provider_modelsA | List the model ids an OpenAI-compatible provider offers (submitter or admin). The daemon calls ``GET {base_url}/models`` with the referenced shared
key, so you can pick a model for ``set_ai_route`` while the key stays on
the machine. Returns ``{base_url_host, models}`` (sorted ids only).
Read-only despite the POST underneath (no idempotency key).
Refusals: 422 ``ai_gateway.base_url_invalid`` / ``ai_gateway.secret_ref_invalid``,
503 ``secret_missing``, 502 ``ai_gateway.provider_unreachable`` /
``ai_gateway.provider_error`` (the HTTP status, never the provider's text).
|
| set_ai_routeA | Create or replace a machine AI gateway alias (submitter or admin). Apps then declare ``[ai.default] provider = "gateway"`` and
``model = "<alias>"``: the daemon injects the gateway URL and a per-app
virtual key, and the provider key never enters a container. Answers
``enabled: false`` while ``[ai_gateway]`` is off (the alias is stored
and serves once it is enabled). Refusals: 422 ``ai_gateway.alias_invalid``,
``ai_gateway.base_url_invalid`` (not https, private address, userinfo),
``ai_gateway.secret_ref_invalid``.
|
| remove_ai_routeA | Remove a machine AI gateway alias (submitter or admin). Refused with 409 ``ai_gateway.alias_in_use`` (naming the apps) while a
running app's ``[ai.*]`` uses it, unless ``force``.
|
| get_ai_usageA | AI gateway usage per (UTC day, app, alias): requests, prompt and
completion tokens, upstream errors. No cost estimate, no prompt trace. |
| get_configA | Read daemon config: one section view, or all sections when omitted. Secret-bearing values are redacted server-side.
|
| set_configB | Write a daemon config section (admin-scoped, validated, audited). |
| apply_configA | Declaratively apply a multi-section daemon config document (all-or-nothing). A dry run needs neither an ``If-Match`` nor an idempotency key.
|
| get_app_configA | Read a deployed app's daemon-persisted config (deploy fields + ai bindings). Secret values are never returned — only ``${secrets.X}`` refs and env
key names. Each ``[ai.<name>]`` binding shown here is injected into the
app at launch as ``NERDIT_AI_<NAME>_URL/_KEY/_MODEL``, and the
``default`` binding also sets ``OPENAI_BASE_URL/OPENAI_API_KEY/
OPENAI_MODEL`` (drop-in OpenAI SDK).
``deploy.release`` is the command run once inside the new image before
traffic moves (a failure aborts the deploy and reverts the image,
never the data — write idempotent migrations); ``deploy.cutover`` null =
zero-downtime swap when eligible, false = same-port swap. ``env_keys``
are literal runtime env names; secret refs are accepted only by
``ai.*.api_key``, ``db.*.password``, ``edge_auth.password`` and a git
``token_ref`` — a ``${secrets.shared.KEY}`` there resolves at launch for
any owner, so a human can place the secret and the agent only names it.
|
| set_app_configA | Write an app's deploy or ai config section (admin/owner-scoped). Writing the ``ai`` section re-wires the app's bindings: on the next
launch/restart each ``[ai.<name>]`` binding is injected as
``NERDIT_AI_<NAME>_URL/_KEY/_MODEL``, and the ``default`` binding also
sets ``OPENAI_BASE_URL/OPENAI_API_KEY/OPENAI_MODEL`` — pass
``restart=True`` to apply it immediately. A dry run needs neither an
``If-Match`` nor an idempotency key.
|
| get_auditA | Read the audit log (admin-scoped), bounded to limit entries per page. Returns an ``AuditLogPage`` dict. Non-admin tokens get a structured
``forbidden`` (403) error.
All filters compose with AND, and all are applied in SQL — narrowing is
cheap, so prefer a filter over paging.
|
| get_eventsA | Use when: you need the daemon's own account of what it just did. The durable feed of autonomous transitions (``service.failed``,
``service.degraded``, ``service.deploy_succeeded``, ``model.ready``,
...), readable by any authenticated token. Two directions, and picking
the wrong one walks the wrong way through history.
|
| capabilitiesA | Describe this daemon: version, caller role/quotas, URL grammar, backends. Role-aware, pure in-memory read — call it first to learn what the
daemon can do (buildpacks, model backends, proxy mode and public-URL
shape, deploy limits, git allowlist, feature flags) instead of probing
endpoints. Admin callers additionally receive ``proxy.admin_addr`` and
the ``paths`` block.
|
| doctorA | Run the daemon's bounded environment health checks (docker/gpu/proxy/db/...). Returns per-check ``{status, detail}`` plus a worst-of top-level
status. Every check is timeout-bounded server-side; details carry key
*names* only, never paths or secret values — safe for every role.
|
| proxy_statusA | Get the embedded proxy's typed state (TLS, apex, respawn, live routes). Authenticated read: proxy state, TLS subjects + CA fingerprint, apex
registration, respawn backoff, and a tri-state live route table read
from the Caddy admin API. Complements ``list_routes`` (the
DB-authoritative inventory).
|
| list_routesA | List the DB-authoritative route inventory, annotated with live Caddy state. Models appear with ``route: null`` (loopback-only by design); each row
carries its live-table annotation.
|
| system_diskA | Report disk usage: docker totals + named-volume/model/archive/backup trees. Any authenticated read. Names and byte counts only (no absolute paths);
lists orphan images and orphan data dirs (reclaim them with
``system_gc``). Concurrent ``du`` walks run under a soft budget — an
overrun returns a partial body with a ``scan_timeout`` warning.
|
| system_gcA | Garbage-collect orphan app images (admin-scoped, audited). Reclaims ``nerdit-app/*`` image repos with no live workload row, verified
by re-listing so an in-use tag is reported skipped, never removed.
|
| restart_daemonA | Restart the daemon to apply restart-required config (admin-scoped, audited). Returns 202 with drain counts. **Over the HTTP MCP transport this kills
the connection serving this call**: a dropped response after issuing it
means the restart most likely started. Confirm with ``capabilities``
(``uptime_s`` resets); retrying with the same idempotency key answers
``409 daemon.restart_in_progress`` or a fresh 202, never a second
restart mid-drain.
|