--- name: nuved description: Build, deploy and operate a project on nuved (nuved.io), where a project is one repository that deploys by push to a machine the platform chooses, every address is assigned by the platform, and files for a person to look at are published as artifacts. Use whenever a task touches a nuved project, its addresses, its machines, its people or its deploys, from an agent token or a person's session. --- # nuved You are working on ONE project of a workspace. A project is one repository on nuved's git (`git.nuved.io//`) with the services as directories inside it and a `compose.yaml` at its top; a push deploys it to a machine the platform chose. The platform assigns every address; you never pick one. Rules are enforced on the machine (admission, limits, fences) and say why when they refuse: read the refusal, do not retry it with different spelling. ## Quick start ```bash nuved whoami # who the credential is, and the balance; proves the CLI is signed in nuved projects # the workspace's projects nuved project flow --json # how code reaches main here (agent.branch, agent.next): read it before you push git clone https://git.nuved.io// # password: an agent token; the username is not checked # … change the code; make sure compose.yaml (or a Dockerfile) is at the top … git push nuved agent/ # the flow says where; the push queues the deploy nuved artifact publish ./mock # a file or folder the owner must look at → https://artifacts.nuved.app// nuved people # who is in the workspace and what each reaches ``` If `nuved` is missing: `curl -fsSL https://get.nuved.io/install.sh | sh`, then `nuved login` (opens a browser; never asks for a password in the terminal). A CI or agent host uses an agent token: `nuved token create --for ` prints one; it reaches that project and nothing else. A token belongs to the person who made it (an owner, or a member who works on the project; never a viewer): it dies when that person is removed or loses the project. `--scope read,artifacts` narrows what it may do (read, code, deploy, logs, artifacts; all when left out) and `--expires 30d` ends it; a refusal names the missing scope. The desktop app (nuved for Mac) does the same things with a window, and offers them to any agent on the Mac over MCP (below). ## What is true about a project - **One repository, one definition.** `compose.yaml` (or `docker-compose.yml`) at the top is what runs; a repository with only a `Dockerfile` gets a one-service compose file generated and committed back on its first deploy. Services are directories inside the repository; an API change and its client ship in one commit. - **Deploy is a push.** The platform's git hook queues a deploy for the project's machine; the machine's agent (nuved-boxd) fetches the commit, reads the definition, applies admission (limits clamped, forbidden settings refused with the reason), runs it under the customer's own Linux user with rootless Podman, waits for health, and reports what runs. The owner sees it in the app's Where it runs; the API has it at `GET /v1/placements`. - **Where it runs is the platform's decision.** Test and preview run on nuved's Hetzner box (compose, rootless Podman per customer); a project moves to Kubernetes when it earns money. A developer may also run **Mansoor OS on this Mac** from the app: a small Linux machine (Ubuntu, Podman, the same agent) that enrols as a machine of the workspace and takes deploys by push with the same rules. You do not choose the machine; the placement record does. - **Addresses are assigned.** A service answers at `-.nuved.app` behind the workspace's login once the platform exposes it; a port on somebody's Mac gets `-.nuved.dev` through a share (one made before 2026-10-14 keeps its `.fromnovid.com` name until it lapses). Flags like `--keep-as` or a chosen hostname do not exist; a requested name is refused. - **People reach what they were given; only owners organise people.** Owners (there may be several) reach everything and are the only ones who invite (to the workspace, a project or nuved), revoke invitations, change roles, remove people and arrange teams; billing and top-ups, machines, and deleting or moving projects are theirs too. Members of the whole workspace work on every project; a project member works on their projects; a project viewer looks and sees secret names but never a value. Work means code, deploy, set and read secrets, and make agent tokens. An invitation always names its scope: some projects, or the whole workspace as a separate confirmed choice (`nuved people invite --project ` repeated, with one `--role member|viewer`, viewer by default; or `--whole-organisation`; with neither it sends nothing). `nuved people invite|role|remove` refuse anyone but an owner. ## The project's repository, and the other agents in it **Read the project's flow before you push, and follow it.** Each project picks how code reaches main, and the platform enforces it on every push; nothing in the repository overrides it. ```bash nuved project flow --json # agent.branch, agent.next, agent.push.allowed, version ``` Push to `agent.branch` (your task in place of ``) and do what `agent.next` says. Under `direct` that is main itself; under `branch-gate` and `branch-dev-main` it is your own branch, and a person merges it. A refused push names the flow and its version in its last line: read the flow again, because it may have changed since you started. The one exception every flow has is the first push into an empty repository, which may create main. **Assume somebody else is pushing to the same branch.** Git refuses to lose their commits: a push that is not a fast-forward is rejected and the branch does not move. That rejection is the system working. - **Never force-push a shared branch.** It is the one command that deletes work already accepted. - **Take a branch of your own for every task**: `agent/`. You may delete the branches you created, and only those. - **Read before you write.** `git fetch nuved && git log --oneline -10 nuved/main` tells you whether the ground moved since you started. - **Claim before you edit.** `nuved claims ` says who works where right now (live, stale after thirty minutes without a heartbeat, done with the commit). A live claim over the paths you need means stop and say so; never work around it. Then claim your own: `nuved claim "" --path --note "" --agent ""`, heartbeat every twenty minutes (`nuved claim heartbeat `), and close it when the work landed (`nuved claim done --commit `). `nuved brief` lists the claims first. The platform records and shows; it refuses nothing on a claim: the flow is what keeps commits safe. - **Sign your commits** if a key is registered for you (`git config gpg.format ssh`, `user.signingkey`), so the project's Code page shows which changes were yours. - **The project's own skill.** A project may carry its own instructions (`AGENTS.md`, `CLAUDE.md`, `.claude/skills/*/SKILL.md`): the stack, the test command, the acceptance check, what must never be done. Read them before this file's general rules; they are the project's word. ## Work from the task board Start an item from its pack: `nuved task brief ` prints what the board and the repository say about it (why, criteria, owned paths, the earlier attempts and send-backs, the instructions files) within a budget; read the source it points at yourself. 1. Which board: one board project per repository, named after it; in the repository, `nuved task` finds it (else `--project `). 2. Take work: `nuved task next` picks and claims the next ready item your role may take; `nuved task claim ` takes a named one. 3. Report within five minutes: `nuved task report ""`; the first report starts the work. 4. Every twenty minutes (ten in the cloud): `nuved task report ""` or `nuved task heartbeat `; a lapsed lease gives the item back. 5. Link as you go: `--commit ` or `--branch ` on a report; put `Task: ` in commit messages. 6. Finished: first publish the proof, the check's commands with their output (screenshots or a recording for a screen), `nuved artifact publish --task --as evidence`; then `nuved task done --commit ` hands it in for review; a spike hands in `--finding ""`. A task or a bug is not closed without its proof, and done says so. 7. Cannot finish: `nuved task release --why ""` gives it back; never sit on a claim. 8. A question, or an unclear task: block the item with it (next section) and keep heartbeating; do not start other work on the same paths. 9. A 403 names your role and the step it may not take: stop, do not retry, report the sentence as it is. 10. Then stop. Review, closing and ranking are other roles' steps. When claim or next says your owner or your team is draining its agents, or that their agents hold their limit, finish what you hold and take nothing new. ## When you have a question A question, or a task you find unclear, is asked on the item, where the operator and the people of the project see it and answer it. Never ask only in a private message, a chat or a message to another session: an answer given there is lost to the team and to the next agent on the item. - Ask: `nuved task block --why ""`. It waits on a person (`--on human`, the default); `--on dependency` or `--on env` when it waits on other work or on something broken. The console lists it under Waiting for you. - While it waits, your claim stays yours: keep heartbeating, and go on with the parts the answer does not touch. - Read the answer: `nuved task show ` prints the questions and notes on the item. A person answers with `nuved task note ""`, or with `nuved task unblock --note ""`, which also lets the work go on. - Your own decision or an answer you found: `nuved task note --decision ""` (or a plain note) on the item you hold, then `nuved task unblock `. ## Keep what is shared on the task What the operator or a teammate shares about a task in a conversation (a link, a file, a decision) goes on the task before the session moves on: whoever comes to the task later reads no chat, message or context of yours. - A link: `nuved task attach --why ""`, `--title "…"` if the address says little. Only http and https addresses are taken. - A file: `nuved task attach --why "…"`; it is published as an artifact on the task (`--as directions|mock|screenshot|report|evidence`, evidence by default), its why noted on the item. - A decision: `nuved task note --decision ""`. - Next work found in the task or a file on it: `nuved task follow-up "" --why "…"` (`--artifact <id>` for the file); the new task says where it came from. - Read it all back: `nuved task show <id>`, and the brief's "References and files" and decisions. - An agent token attaches and notes only on an item it holds; a planner notes any item of its project. ## Review an item on the task board 1. Take the review: `nuved task claim <id> --review`; the board never gives you the review of your own hand-in. 2. Read what the worker was told: `nuved task brief <id> --saved`, then the hand-in's commit, the criteria and the done rules. 3. Report within five minutes and every twenty after, as a worker does: `nuved task report <id> "<what you check>"`. 4. Check by running: the item's acceptance check and the tests, with the exit codes; a diff read alone is not a review. 5. Pass: publish your report first, what you ran with its output and exit codes, `nuved artifact publish <report> --task <id> --as evidence`; then `nuved task verdict <id> pass`. A verdict never closes the item; the gate or a person does. 6. Send back: `nuved task verdict <id> send_back --note "<what to change>" --file <f> --line <n>`, one note a worker can act on. 7. Never fix it yourself: the fix is the worker's next attempt, and your note is what it starts from. 8. Cannot finish: `nuved task release <id> --why "<why>"`. ## Plan the task board 1. Split a story into tasks: `nuved task new task "<title>" --parent <story> --why "…" --ac "…" --class standard --path <dir> --est <min>`. 2. Each task owns paths no other open task owns, so two agents never edit one file; `--depends-on <id>` orders the rest. 3. A criterion is one a reviewer checks by running something: a command, its output, a screen at an address. 4. Make it ready: `nuved task move <id> ready`; the board names what is still missing. 5. Accept an agent's item from triage with `nuved task move <id> backlog`; rank with `nuved task rank <id> --before <id>`. 6. Read the pack an agent will get: `nuved task brief <id>` fits its budget and names the paths and criteria it needs. 7. Never claim code work or a review, and never move an item out of hard-core: that is a person's word. 8. Say on the item why you moved it, and answer a worker's question there: `nuved task note <id> "<text>"` (`--decision` for a decision). A planner token notes any item of its project without holding it; it does not report, block or unblock. ## Your own folder: nuved agent Whoever starts an agent gives it a folder of its own first (docs/agents.md in nuvedctl): ```bash nuved agent up <item> --name <role>-<area>-<n> [--token-file <agent token config>] source ~/nuved/agents/<workspace>/<project>/<name>/.nuved/agent.env # before the agent starts nuved agent list # who is up, with which ports nuved agent down <name> # once the branch has landed ``` - Work in your folder, on your branch `feature/<item>-<name>`; never in the project's `base` clone. - Serve on `PORT_BASE` (and the ports up to `PORT_LAST`), run the test database on `TESTDB_PORT` with `PGDATA`, and keep scratch files in `TMPDIR`: those are yours alone. Never a fixed port. - `nuved` in your folder is your agent token (`NUVED_CONFIG`), or signed out; it is never the person's session. - `down` refuses while your branch has commits in neither dev nor main, changes are uncommitted, or something of yours still listens; it says which. `--force` is the person's call, not yours. ## Writing a service that deploys here - Listen on the port your service names in `compose.yaml` (`ports:` or the `nuved.io/port` label); the platform's edge reaches that port on the machine's internal network and nothing else. - Exactly one service is the public one: give it the label `nuved.io/public: "true"`. Only that service may have `ports:`; admission refuses `ports:` on any other ("services reach each other on the project's network"), so a database or a worker names no port at all. - Give every service a health check (`healthcheck:` in compose); the deploy waits for it and reports it; a service without one shows "no health check" and nobody knows when it is up. - Expect limits. Admission clamps memory, CPU and process counts to what the project may use and says so in the deploy's words; a service that needs more needs a person. - Keep state in named volumes; the machine keeps them across deploys. Put nothing on the container's own filesystem that must survive. - Read secrets from the environment or from a file when you connect, and expect them to change (rotation comes with the database half of Riegel Secrets). The project's own secrets are in its vault on the platform: `nuved secret get <project> KEY` (the section Secrets below). - Images: public ones pull as they are; private registries need credentials the owner adds. ## Before you report "deployed" - The deploy's own words: the app's Where it runs row for the service says live and healthy with the commit you pushed; `GET /v1/placements?project=<project>` says the same (`status`, `health`, `commit`, `address`, `last_error`). - If exposed: `curl -fsS https://<service>-<project>.nuved.app/` answers from outside (behind the workspace's login; your own session cookie or a token the owner gave you). - A `failed` row with `last_error` is the machine's sentence: read it whole; it names the service and the rule. A row that stays `queued` means the project's machine is offline; say so, do not wait in silence. ## Before the merge: a preview of your branch `nuved preview up` (the branch you are on; `--branch b`, `--task GG-142`) runs your branch beside production on the project's machine and prints its address when it is live. Push the branch first: the machine fetches it from the project's git. - It is its own environment, `preview-<branch>-<6 hex>`: its own containers, a network closed to every other project and to production, and a new database. Production is never touched. - Its database starts empty, your branch's migrations run as the program starts, then the seed is loaded once: a database service's `nuved.seed` label (a `.sql` file or a folder of them in the repository), else `seed/<service>.sql`, else `seed/<service>/`, else `seed/` when the project has one database. When your feature needs data to be seen, add it to the seed in the same branch. A seed that fails fails the preview with the file and the database's words. - The address is behind the workspace's sign-in, like every share; an agent token does not pass that sign-in yet, so a person or a signed-in browser opens it. With `--task` it is linked to the item once live. `nuved preview ls` lists them; `--json` on `up` and `ls` is for scripts. - After the merge: `nuved preview down` takes away its containers, its database, its checkout and its address. ## Showing somebody a file: artifacts A file or folder made to be looked at (an HTML mock, a report, a screenshot) is published as an artifact, never served from a laptop, a share, a tunnel Service or a fixed name. The link is what the project's owner opens to confirm a design. ``` nuved artifact publish mock.html # one file nuved artifact publish ./design/mocks --title "Where it runs" # a folder: index.html at its top nuved artifact ls nuved artifact confirm <id> # the owner's yes; the file goes a week later nuved artifact reject <id> nuved artifact pin <id> # keep it; unpin gives it a fresh expiry nuved artifact rm <id> ``` A **mock of a screen of nuved for Mac is a story, never hand-drawn HTML**: a Storybook story of the app's real components with the test fakes as data (nuved-desktop `frontend/src/design/*.stories.tsx`, one story per state, framed by a line naming the surface, the screen and the state), built and published with `tools/design/publish.sh`. The owner confirms the story's artifact. A mock of another surface (the web console, the CLI) says so in its first line. The address is `https://artifacts.nuved.app/<id>/`: 128 random bits, no index, open to the workspace's people after the console's sign-in and to nobody else. An artifact expires after 14 days (`--days`, up to 90); at most 15 MiB and 500 files; a folder needs an index.html at its top; hidden files and links are left out. The record (title, who, decision, sha256) outlives the file: write the decision into your own notes at once. An agent token may publish and list; confirming, pinning and removing are a person's. The desktop app lists them and decides there. ## Secrets A project's secrets (a database URL, an API key, an SMTP password) live in the project's own vault on the platform (OpenBao behind nuved's API), as names with versions. They are never in the repository, never in `compose.yaml`, never in a log or a list. ``` nuved secret ls <project> # names, version, who set each and when; never a value nuved secret get <project> DATABASE_URL # the value alone, for a program or a script: $(…) works nuved secret get <project> DATABASE_URL --version 2 printf %s "$v" | nuved secret set <project> KEY # a person sets: stdin, --from-file, or a hidden prompt nuved secret rm <project> KEY # every version goes ``` Who may: an owner, a member, or a project member of that project sets, reads and removes; a viewer sees the names only and is refused a value; the project's agent tokens read (`nuved token create <project> --scope secrets` gives one that reads secrets and nothing else; a token without the `secrets` scope is refused them); an agent token of another project is refused by the vault's own policy, not by a rule you can argue with. Every set and remove is in the project's activity. A value never goes on a command line (`set` reads it from stdin, a file or a prompt), and `get` prints it with nothing else, so pipe it into what needs it rather than into your notes or a reply. The desktop app's project page lists the names and who set them; it shows no value either. ### Databases: a user that dies with its lease, a root nobody knows Declare the database in `compose.yaml` and let the platform own its root: ```yaml services: db: image: postgres:16-alpine environment: { POSTGRES_DB: app } # no password here: the agent makes it volumes: [data:/var/lib/postgresql/data] labels: nuved.secrets.db: postgres # the platform's agent gives it its root and hands it to the vault nuved.secrets.rotate-root: "true" # then rotates it away: nobody knows it, admin creds are the way in api: labels: nuved.secrets.env: DATABASE_URL=db/db:app,API_KEY=kv/API_KEY # VAR=source: a db role, or a secret nuved.secrets.file: /run/nuved/secrets/app.env # the same lines as a file nuved.secrets.reload: sighup # restart (default) | sighup | none ``` On deploy the agent renders `DATABASE_URL=postgres://<user>:<password>@db:5432/app?sslmode=disable` with a fresh one-hour `app` user (renewed up to a day), checks it opens a connection, and only then starts the service; it rotates the credential before it ends and whenever a person revoked it, in a fixed order: new credential, a checked connection, the file rewritten, the reload, then the old lease revoked. The one-sentence contract for your program: **read the secret from the file or the environment when you connect, and expect it to change.** Under `restart` nothing more is needed (the container is recreated: about a second without answers). Under `sighup` re-read the file and swap your pool on the signal: no request is lost. Under `none` re-read on the first failed query. `nuved.secrets.env` reaches the environment and cannot change in place, so it means `restart`; `nuved.secrets.file` can change, so `sighup` and `none` need it. ``` nuved db ls <project> # the databases and their roles nuved db creds <project> db # a one-hour app user, printed once (agents: this, with scope secrets) nuved db creds <project> db --admin --psql # a fifteen-minute superuser in psql: owners only, break glass nuved secret leases <project> # every live credential: role, since, until; never a password nuved secret revoke <project> <lease> # its sessions end within a second; the agent re-mints at its next tick nuved db rotate-root <project> db # owners: the root to one only the vault knows ``` A program can do all of this itself with one package instead of the agent's file: `riegel` for Go (`github.com/nuved/plans-backend/sdk/go`), `@nuved/riegel` for Node, `nuved-riegel` for Python (plans-backend `sdk/`). Each presents the project's agent token (set it as a secret and deliver it: `nuved.secrets.env: NUVED_TOKEN=kv/NUVED_TOKEN`), mints a short user through nuved's API, hands the program a pool, renews at two thirds and rotates in the fixed order by itself; no file, no reload label. A minted password is printed exactly once and stored nowhere; pipe it where it is needed. Engines: `postgres` (the URL is `postgres://…`), `mariadb` (`nuved.secrets.db: mariadb`, the root from `MARIADB_ROOT_PASSWORD` which you do not set, the URL `mysql://user:pass@db:3306/dbname`) and `redis` (`nuved.secrets.db: redis`, the URL `redis://user:pass@db:6379/0`; the agent starts redis-server from a config file it renders with the root, your `command` may only be arguments such as `--appendonly yes`; `rotate-root` is refused for Redis, and its ACL users live in memory: a restart of the Redis container loses the vault's users until the next rotation). A declared `mysql`, `mongodb` or `rabbitmq` is refused with the sentence that says so (MySQL has no way to end a revoked user's sessions by statements; MongoDB has no plugin in OpenBao's catalog; RabbitMQ comes next). Never set the root password in the file: a declared database is refused with it. A database on a Mac's machine is not delivered yet. ### The workspace's vault: its keys are a human's Every project's secrets and databases live in the workspace's own vault on a machine the owner chooses, and its shares are held by people, never by the platform. `nuved vault status` says which machine holds it, whether it is sealed and which holders have their share. Every holder first publishes a key: `nuved key` (or opening the nuved app) makes this device's key pair once, keeps the private halves in `holder.key` (0600) and publishes only the public halves. An owner then makes the vault: `nuved vault init --machine <id or name> --holders a@x,b@y --threshold T [--stay-unsealed]` (without `--holders`, one key held by the owner). The machine makes the shares and encrypts each to its holder's key in its own memory; nuved only relays ciphertext it cannot read. Each holder runs `nuved vault share` on their device: the share is shown once, a copy stays there (`vault-share-<org>`, 0600) unless `--no-keep`, and they keep it in Riegel too. A holder who loses every device and every copy loses their share; with one key, that loses the vault's data. No root token is kept: it is revoked on the machine after the setup. Break glass will be a new root made from the shares, which is not built yet. After the machine restarts the vault is **sealed**: secret, database and lease routes refuse with 503 and say so, `nuved vault status` says SEALED, and a deploy that needs a credential waits and says why. Each holder runs `nuved vault unseal` until the threshold; the share is encrypted to the machine's key and signed by the device. The first time, it prints the machine's key line and the holder compares it with what `nuved-boxd fingerprint` prints on the machine (over ssh for nuved's box) before anything is sent; a changed key later is refused until compared again with `--trust-new`. An agent never runs `vault init`, `vault share` or `vault unseal`: those are the owner's and the holders' own acts. Tell them, in words, which command they run. Never print, log or store a share you were handed. ## Sharing a port from a Mac A port on somebody's Mac gets a public address behind a login from the app (This Mac → Share a port) or `nuved share`; the address is assigned, kept while the share runs, and held for a while after. It is for showing a running thing to a person for an hour, not for serving a mock (that is an artifact) and not for a service (that is a deploy). ## Agents through the app (MCP) nuved for Mac serves an MCP endpoint on the Mac's loopback, behind a token it shows under **Agents** on This Mac. Any MCP client the developer uses, with whatever model and provider they have, connects with one line (Claude Code: `claude mcp add --transport http nuved <url> --header "Authorization: Bearer <token>"`) and acts through the signed-in session: `nuved_brief` (read it first: projects, machines and what runs, artifacts awaiting the owner's word, people), `nuved_projects`, `nuved_project_create`, `nuved_where_it_runs`, `nuved_machine`, `nuved_artifacts`, `nuved_artifact_publish`, `nuved_shares`, `nuved_share_add`, `nuved_share_remove`, `nuved_people`. Everything an agent changes shows in the window under Agents. Nobody signed in to the app means nothing works there; say so rather than guess. ## Projects the platform placed on Kubernetes A project on Kubernetes is one namespace behind a proxy at kube.nuved.io; `nuved kubeconfig <project>` prints a kubeconfig that reaches it and nothing else. There the rules are the quota's: caps are per project and are the sum of container requests; every pod runs as non-root, drops all capabilities, uses the RuntimeDefault seccomp profile, and is owned by a Deployment, StatefulSet, Job or CronJob (a bare Pod is refused); `Service` of type LoadBalancer or NodePort is refused, the address comes from `nuved expose <service>`; a request above the shared max (`GET https://api.nuved.io/v1/rates`, `shared_max`) is refused with the number to use. Read `kubectl describe resourcequota workspace-quota` before every change; `kubectl rollout status` before you say deployed. Money is a prepaid balance debited every ten minutes for what is reserved; at zero every Deployment is scaled to zero until a top-up, which an agent token cannot do: report to the human. ## Do not - Do not paste a secret into a repository, a compose file, an artifact, an activity line or a reply; read it with `nuved secret get` where it is needed. - Do not store an agent token, a kubeconfig or any credential in the repository or an artifact. - Do not ask for, invent or hard-code an address or a hostname; the platform assigns every one. - Do not retry a refusal with different spelling; it is a boundary, and its sentence says which. - Do not serve a mock from a laptop or a share; publish it as an artifact. - Do not fix anything on a machine by hand; the deploy, the definition and the platform are the only ways a machine changes.