Appearance
The Declarative Contract
Everything Nanokit does starts from a single file: nanokit.yml. It is not a template or a script generator — it is the complete description of desired state, and the only thing an agent (or a human) needs to write to change infrastructure.
yaml
name: nanokit
activeEnv: local
env:
SMTP_HOST: '${SMTP_HOST:-service://mail}'
databases:
nk_db_postgres:
engine: postgres
mode: local
rootPassword: '${DB_PASSWORD_POSTGRES}'
branching:
enabled: true
services:
api-gateway:
runtime: node
path: ./apps/api-gateway
start: npm run start
expose: 4000
updateStrategy: recreate
docs:
runtime: static
path: ./dist/apps/docs
expose: 80Three things in that example matter to an agent:
- References, not values.
${DB_PASSWORD_POSTGRES}andservice://mailresolve at apply time — from the environment or the secrets vault. Secrets never enter the file, so an agent can write config without ever handling credentials. - Declarations, not steps. There is no "restart after env change" instruction. The reconciler derives what must happen from the difference between this file and reality.
- One file, many targets.
environmentsoverride slices of this document per context;deploy targetsdecide where a context lands. Same contract, laptop or cloud.
From file to outcome
When the config changes (or nkapp up / nkapp plan runs), Nanokit executes a deterministic loop:
- Observe — query the active provider for current state.
- Diff — compare desired vs. current, gated by drift hashes so unchanged resources are provably skipped.
- Plan — produce a typed plan of
CREATE/UPDATE/DELETE/NO_OPactions, ordered by the dependency graph. - Execute proportionally — touch only what the diff justifies; health checks gate each step.
- Report — expose resulting state (see Machine-Readable State).
The properties an agent can rely on:
| Property | Guarantee |
|---|---|
| Determinism | Same config + same current state ⇒ same plan, every time |
| Idempotence | Re-applying is a no-op when drift hashes match |
| Proportionality | A one-service change produces a one-service deploy |
| Reviewability | Every decision is a YAML diff a human can read before it executes |
Two properties that are commonly assumed and are not guaranteed:
"Constrained scope" is about shape, not authority
The schema constrains what can be declared, not what can be executed. The contract itself expresses free-form shell in six places — services.*.start, services.*.command, services.*.hooks.*, provisioning.install, cron.*.command, and deploy.*.prepare (which runs on the remote host). An actor able to write nanokit.yml and apply it can run commands on the host and on every deploy target.
Bounding that is the job of the agent tool layer, not the config. The MCP server is that layer: an agent's write to any of these fields is refused until a human approves that exact request.
Rollback is manual and one step deep
nkapp deploy --rollback restores the previous reconciler state — the recorded container image and environment — from a local backup state file. It is not automatic: a failed apply does not trigger it. It requires that backup file to exist. And it does not revert code on the remote host or restore database contents; for data, use nkapp backup / nkapp restore.
What this means in practice
An agent that needs "a staging environment with Redis" does not run twelve commands. It edits the contract — add the database block, maybe an environment override — and lets reconcile do the rest. If the edit was wrong, nkapp plan shows it before execution.
The full schema (services, databases, runtimes, networks, cron, volumes, infra providers) is documented under Configuration.
Typed primitives on top of the contract
An agent does not have to edit YAML by hand. The MCP server offers typed intent tools: create_environment, create_service, create_database, create_domain, set_secret, update_service and the matching deletes. They write this same file through a closed field allow-list and return the YAML diff of every change. Editing nanokit.yml directly and invoking the CLI keeps working as before.