Files
windmill/backend/AGENTS.md
T
hugocasaandClaude Opus 5 c3b2275864 docs(agents): rework agent context, fix dev-env docs, vendor skills (#10667)
* docs(agents): scope agent guidance to where it loads

AGENTS.md loads in every session. Three of its sections only ever applied to
one directory, and docs/autonomous-mode.md was unreferenced by anything in the
repo, so none of its content was in effect.

- Move "Verifying Backend Changes" to backend/CLAUDE.md, "Verifying Frontend
  Changes" and "Banned Patterns" to frontend/CLAUDE.md. They now load when
  working under those directories, which is when they apply.
- Update the two cross-references that pointed at the moved sections (pr and
  svelte-frontend skills).
- Delete docs/autonomous-mode.md. Its "don't stop early" half is already in
  .webmux.yaml's oneshot system prompt, which actually loads; its trigger was
  bypassPermissions, which does not imply an absent user; and it restated
  AGENTS.md and the pr skill with copies that had drifted (hardcoded ports,
  relative screenshot paths). Salvaged the UI traps it uniquely documented
  into frontend/CLAUDE.md and dropped the three stale profile references.

AGENTS.md drops ~3.6k characters with no guidance lost.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs(agents): guidance for building a feature — reuse, telemetry, live verification

Three recurring gaps, all cases where a pointer existed but nothing triggered
on it.

Component reuse. The svelte-frontend skill documented three components with
props, which reads as the whole catalog; the barrel exports 23 and common/ has
34 subdirectories against those 23. So "never use raw HTML elements" was an
instruction agents could not follow. Added a mandatory discovery step: read
the barrel, grep the tree, and treat the documented three as examples.

Brand guidelines. frontend/brand-guidelines.md is 34k characters referenced by
bare path, which nothing opens speculatively. Added a table mapping what you
are building to the section that governs it, entered with grep rather than a
full read.

Product telemetry. feature_usage has 14 registered actions across three
features, and an unregistered (feature, kind) pair is dropped by
valid_feature_usage_event with a bare continue — no error, still a 204 — so
frontend-only instrumentation silently records nothing. New
docs/feature-telemetry.md carries the criteria for when to instrument, the
four-step recipe including the allowlist and the InstanceSettings disclosure,
and the privacy rules. Raised in the plan for user-facing work, not as a
separate question, and not at all for bugfixes or refactors.

Also: validation now ends at exercising the change on the running instance,
with standing permission to spin up whatever that takes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(dev): correct the worktree dev-environment guidance

Several things agents were told to do did not match what the machine does.

- Env discovery pointed at .env / .env.local / backend/.env. In a webmux
  worktree the real values are in $(git rev-parse --git-dir)/webmux/runtime.env
  (BACKEND_PORT, FRONTEND_PORT, DATABASE_URL, CARGO_FEATURES, WM_DB_NAME),
  sourced by every pane and undocumented. Reading it is also not blocked by the
  Read(**/.env) deny rules, which the old instruction walked straight into.
- The database name rule said branch-with-underscores. worktree-common.sh uses
  the worktree directory basename, and Postgres truncates at 63 characters, so
  branch hugo/win-2340-… resolves to windmill_win_2340_…_and_eval with no hugo_
  prefix and the tail chopped. A wrong DATABASE_URL guts the sqlx cache.
- The restart procedure said "tmux pane 1" and sent keys to an undefined
  <pane1>. Pane 1 is the backend under the full profile and the frontend under
  frontendOnly. Replaced with finding the pane by pane_current_command,
  recovering the live feature set from the running process (CARGO_FEATURES in
  runtime.env only records what the pane started with), and restarting in place.
- Added recovery for an orphaned backend holding the port: it reparents to
  systemd when its shell dies, so it survives anything that looks like cleanup.
  Three checks before killing a single pid, because pkill -f windmill takes out
  every sibling worktree.
- Agents spawned their own servers because AGENTS.md opened by telling them to.
  Now it checks for the existing panes first; the spawn commands are scoped to
  a plain checkout.
- New EE worktrees branched from the EE repo's local main, which nothing
  fast-forwards, so they started behind the commit pinned in
  backend/ee-repo-ref.txt — the one CI builds against. They now base on the pin,
  falling back to main only when it is unreadable.
- Enabled webmux autoPull so local main stays current; new worktrees are
  branched from it. Documented what WM_CLONE_DB does, including that it
  terminates every connection to the base windmill database.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* feat(skills): vendor grilling/architecture skills; tighten PR ready and review rounds

Vendors five skills from https://github.com/mattpocock/skills (MIT, pinned at
84fdeffd12f2ee307994d1eb6feb48173b6e0502). They are one dependency closure:
grill-me is a stub that runs grilling, and improve-codebase-architecture draws
its vocabulary from codebase-design and its CONTEXT.md upkeep from
domain-modeling. .agents/skills/UPSTREAM.md records the license, the pin, and
the four local deltas so a refresh stays a diff:

- flattened the upstream engineering/ and productivity/ split
- rewrote bundled-file links to repo-root paths, since relative links break
  when read through the .claude/skills symlink
- dropped the upstream agents/openai.yaml packaging metadata
- removed every ADR path. This repo has not adopted ADRs, and a skill that
  offers to create them is how the practice arrives by side effect rather than
  by decision.

PR workflow changes, all in the pr skill:

- A round that never starts is usually a conflict with main, not a CI outage.
  Resolve by merging, not rebasing — a rebase rewrites the head SHA that round
  verdicts and the clean-round marker are keyed to. If the merge advances
  backend/ee-repo-ref.txt, the EE worktree has to follow or
  cargo check --features private compiles a tree neither the author nor CI
  intends.
- A clean round no longer means an automatic flip to ready. Wide blast radius
  (*_ee.rs, migrations, OpenAPI or the generated client, auth paths, shared
  worker infrastructure, a new public surface) asks first; self-contained
  changes flip. Unattended, the judgement holds and the action degrades: flip
  the small ones, leave the rest at a clean draft with the reason in the PR
  body.
- Rounds that never converge are usually structural. After three without
  convergence, stop, name the module the findings cluster around, and suggest
  improve-codebase-architecture rather than burning more CI.

AGENTS.local.md (gitignored, with CLAUDE.local.md importing it) holds the
ready/ask calibration, recorded as dated observations rather than a rule.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs(dev): state that each worktree gets its own fresh database

The per-worktree section warned which DATABASE_URL to use but never said where
the database comes from: the post-create hook creates and migrates a new one
per worktree, so it starts with none of the main instance's workspaces, scripts
or flows. WM_CLONE_DB was documented only as a comment in .webmux.yaml, which
reads as how things work rather than as a per-project opt-in.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* chore(sqlx): script the cache backup/restore instead of documenting it

The update-sqlx skill spelled out a cp/comm/rm dance around `cargo sqlx
prepare`, which empties backend/.sqlx before regenerating — a failed run leaves
the cache gutted (observed: 2350 -> 142 entries), and a --all-targets run in a
CE checkout fails that way every time. Three problems with documenting it:

- The backup path was the literal /tmp/sqlx_backup, shared by every worktree.
  Two concurrent runs overwrite each other's backup, which is the only thing
  standing between a failed prepare and a gutted cache.
- The restore was a copy-pasted `rm -rf .sqlx && cp -r ... && cp ...` chain.
- Skipping the backup is what turns a routine failure into a lost cache, and a
  convention is easier to skip than a command.

sqlx-cache.sh has backup / newq / restore, keeps state in a per-worktree
directory, and leaves the judgement call where it belongs: `newq` prints each
added entry's query field for review, and only `restore` writes them in.

Also adds the general rule that scratch files belong outside the checkout —
anything written into the tree has to be deleted again, and rm prompts each
time.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs(agents): state why a routine cleanup prompts, and where scratch goes

The guard hook already auto-allows a plain rm whose operands are under /tmp or
inside a git checkout in $HOME, so deleting a temp dir or a stale .sqlx entry
costs nothing. What prompts is the command shape: the hook's tokenizer defers on
&&, ;, redirects, quotes and $VAR, so a chained cleanup falls through to the
Bash(rm:*) ask rule.

That was recorded only inside a paragraph about screenshot file paths in
frontend/CLAUDE.md, where nobody looking for it would find it. Stated in Core
Principles instead, alongside the rule that scratch belongs outside the tree —
for the reason that actually applies, which is not committing junk rather than
avoiding prompts.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* chore(security): deny agent edits to the permission hooks and project settings

.claude/hooks/guard-rm-outside-tmp.sh and guard-main-branch.sh are the
enforcement points for everything the permission rules are meant to catch, and
nothing stopped an agent editing them. One sed -i disables the guard for every
later command, silently, and the deny list in .claude/settings.json has the same
exposure.

Defence in depth rather than a boundary: an agent with arbitrary bash can still
delete, and this may only close the Edit-tool path if Bash writes are not
covered by Edit deny rules. It costs nothing and removes the cheapest way to
turn the guards off. Changing them now means editing the files by hand, which is
the intent.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: address review round findings on head 3f47dc1

- backend/ and frontend/ guidance was Claude-only. Codex and Pi read AGENTS.md,
  not CLAUDE.md, so moving "Verifying Backend/Frontend Changes" and the
  $bindable ban out of the root AGENTS.md made them invisible to two of the
  three CLIs this repo supports. Renamed both to AGENTS.md with a one-line
  @AGENTS.md CLAUDE.md beside them, matching what the repo already does at the
  root and in ai_evals/, and retargeted the four references.

- sqlx-cache.sh aborted with exit 2 and no output when .sqlx was empty:
  list_entries ran `ls -1 ./*.json`, and an unmatched glob under
  `set -euo pipefail` killed the script. An empty cache is precisely what a
  failed prepare leaves behind, so it broke in the one case it exists for.
  Replaced with a glob loop; reproduced the failure and verified the fix.

- The oneshot prompt ("never leave the PR sitting in draft") contradicted the
  "Flip, or ask first" rule added in the same PR, which tells unattended runs to
  leave wide-blast-radius changes as clean drafts. The prompt now defers to the
  skill for the flip decision and keeps only "never stop at an unreviewed
  draft".

- Bundled-resource references in the vendored skills were markdown links to
  `.agents/skills/...`, which resolve relative to the file, not the repo root.
  Replaced with inline paths stating they are repo-root relative.

- The PR-ready calibration file was write-only: the skill said to record
  answers there but never to read it. It is now consulted before deciding.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Revert "chore(security): deny agent edits to the permission hooks and project settings"

This reverts commit 3f47dc1692.

* fix: address round 2 nits

- backend/AGENTS.md told agents to persist CARGO_FEATURES in runtime.env, but
  webmux regenerates that file from metadata and .env.local every time the
  worktree is opened, so the setting is lost on the next reopen. The persistent
  source is .env.local, which scripts/post-create.sh already writes.

- UPSTREAM.md still described the vendoring delta as rewriting bundled-file
  *links* to repo-root paths. 555f063 replaced them with plain paths in prose,
  because a markdown target resolves relative to the file — a repo-root link is
  just as broken as a sibling-relative one through the symlink. Replaying the
  old wording on a refresh would reintroduce the bug UPSTREAM.md exists to
  prevent.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs(skills): correct the UPSTREAM.md link-rewrite delta

The delta note still described rewriting bundled-file *links* to repo-root
paths. 555f063 replaced them with plain paths in prose, because a markdown
target resolves relative to the file containing it — a repo-root link is as
broken as a sibling-relative one read through the symlink. Replaying the old
wording on a refresh would reintroduce exactly the bug UPSTREAM.md exists to
prevent.

The preceding commit's message claimed this fix; the edit had failed on a
stale anchor and only the backend/AGENTS.md half landed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs(dev): describe what a fresh worktree database actually contains

Exercising a real worktree creation showed the previous wording ("none of your
workspaces, scripts or flows") reads as an empty database. It is a bootstrap
instance: the admins workspace, the admin@windmill.dev superadmin, the license
key copied from the base database, and the migration seeds — observed as
u/admin/hub_sync and the default app theme resource.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-13 11:12:20 +00:00

224 lines
14 KiB
Markdown

# Backend (Rust)
- **Coding patterns**: MUST use the `rust-backend` skill when writing Rust code
- **Validation**: `docs/validation.md` — which `cargo check` flags to use
- **Enterprise**: `docs/enterprise.md` — EE file conventions and PR workflow
- **DB schema**: `backend/summarized_schema.txt`
- **API routes entry point**: `windmill-api/src/lib.rs`
- **OpenAPI spec**: `windmill-api/openapi.yaml`
- **DuckDB local jobs**: build the dynamic FFI library before running DuckDB scripts locally:
```bash
cd backend/windmill-duckdb-ffi-internal && ./build_dev.sh
```
Re-run after clean builds or when `target/debug/libwindmill_duckdb_ffi_internal.*` is missing.
The bundled DuckDB compile (~2min) is cached in a per-user dir shared across
worktrees (keyed by the crate's `Cargo.lock`), so a fresh worktree reuses it and
the build is near-instant — you don't pay the full compile per worktree. Editing
the FFI crate's own source falls back to an isolated per-worktree `./target`.
The engine is a **patched fork** of duckdb-rs, not the crates.io crate — read
`docs/duckdb-isolation.md` before bumping it or touching the isolation transform.
- **Running data pipelines (DuckLake) from source**: see the section below — a plain build
advertises the `duckdb` tag but cannot execute DuckDB scripts and has no working S3 proxy.
## Cargo features & running the dev backend
The dev backend runs under `cargo watch` and is launched by default with **only
`--features quickjs`** (see the tmux backend pane). That baseline compiles fast but
**deliberately omits most functionality** — notably S3/object storage, the S3 proxy, all
EE code, MCP, and every non-JS language runtime. A running server never gains a feature you
didn't compile in: feature-gated routes 404 or return a `"requires <feature>"` stub. So if
you touch code behind a feature gate, or need to *exercise* such a feature at runtime, you
MUST **restart the backend with the appropriate features** for what you're working on.
### Restarting the dev backend with the right features
Restart in the **same pane**, so the relaunch inherits that pane's `DATABASE_URL`, `BACKEND_PORT`
and the rest of `runtime.env`. Scope every kill to this worktree — **never**
`pkill -f target/debug/windmill`, which kills every sibling worktree's backend.
1. Find the backend pane by what it is running, not by index. The index depends on the webmux
profile: pane 1 is the backend under `full`, but the *frontend* under `frontendOnly`.
```bash
WIN=$(tmux display-message -p -t "$TMUX_PANE" '#{window_id}')
tmux list-panes -t "$WIN" -F '#{pane_index} #{pane_current_command} #{pane_pid}'
```
2. Read the feature set it is **actually** running. `CARGO_FEATURES` in `runtime.env` is only what
the pane started with, and goes stale the first time anyone restarts by hand:
```bash
ps --ppid <pane_pid> -o args=
# /home/hugo/.cargo/bin/cargo-watch watch -x run --features quickjs
```
3. Stop it and relaunch with the extended set. `PORT` in the pane shell can be stale, so pass it
explicitly:
```bash
tmux send-keys -t "$WIN.<idx>" C-c
tmux send-keys -t "$WIN.<idx>" 'PORT=$BACKEND_PORT cargo watch -x "run --features quickjs,private,parquet"' Enter
```
Carry over every feature the old command had unless you mean to drop one — rebuilding the list
from memory is how a backend silently loses `quickjs`.
4. Persist the new set so a recreated pane starts with it: set `CARGO_FEATURES` in the
worktree's `.env.local`, which `scripts/post-create.sh` writes and webmux reads. Do **not**
edit `runtime.env` for this — webmux regenerates it from metadata and `.env.local` every time
the worktree is opened, so an edit there is lost on the next reopen. Either way the change
only affects a future pane; step 3 is what takes effect now.
5. Re-capture the pane until `health check completed` appears before hitting the API. A cold
rebuild takes ~60s, and the previous run's success line is still in the scrollback, so a
capture taken too early reads as ready when it isn't.
cargo-watch only re-runs on a file change, so after an idle/failed run `touch README.md` (from
`backend/`, where the watch runs) is a cheap retrigger (touching a `.rs` forces a full rebuild).
### An orphaned backend is holding the port
If the pane's `cargo watch` looks alive but the API never answers, or the build ends in an
address-already-in-use error, a backend from an earlier run is probably still bound to the port.
It gets reparented to `systemd --user` when its shell dies, so it survives everything that looks
like a cleanup.
Confirm all three before killing anything — a dozen sibling worktrees run their own backend, and
`pkill -f windmill` (or `-f target/debug/windmill`) kills every one of them:
```bash
ss -ltnp | grep ":$BACKEND_PORT" # 1. which pid holds the port
readlink /proc/<pid>/cwd # 2. must be THIS worktree's backend/
ps -o ppid= -p <pid> # 3. parent is systemd/pid 1, not your pane's cargo-watch
```
Only when the port owner is this worktree's backend **and** it is orphaned, kill that single pid
(`kill <pid>`, then `kill -9` if it does not exit). Ask first when there is a human in the loop;
unattended, the three checks are what make it safe. Then `touch README.md` to retrigger the
watch.
### What each feature gate does (the ones you'll actually toggle)
`backend/Cargo.toml` `[features]` is the source of truth; this is the practical dev map. Combine
only what you need — build time scales with the set.
| Feature | Enables | Need it for |
|---|---|---|
| `quickjs` | Embedded JS engine for inline JS eval (the default dev baseline). | Keep in every dev set. |
| `private` | Compiles the `*_ee.rs` files (symlinked from `windmill-ee-private`). Gates **all** EE code, including the real S3 helpers, the S3 proxy, and advanced S3 permission checks. | Any EE code path, S3/object storage. |
| `enterprise` | EE business logic (autoscaling, SAML hooks, advanced S3 rule **enforcement**, WAP, forks, …). Pulls in `license`. | Running EE features. Advanced S3 permission rules only take effect with this. |
| `license` | License-key/plan plumbing (`LICENSE_KEY`). Pulled in by `enterprise`. Having the feature compiled does **not** require a license *key* at runtime — CE defaults to a free plan and most EE paths still run keyless. | License-gated behavior. |
| `parquet` | S3/object-storage support: the `job_helpers/*` and `apps_u/*` S3 endpoints, parquet/CSV preview, workspace large-file storage. Without it those routes return `"requires parquet"`. | Anything touching S3/object storage or datasets. |
| `duckdb` | DuckDB script executor (also needs the FFI dylib — see above). | DuckDB scripts, DuckLake. |
| `python` `rust` `php` `java` `ruby` `csharp` `nu` `deno_core` `mysql` `mssql` `bigquery` `snowflake` `oracledb` `rlang` | Each enables that language/DB runtime for job execution. | Running jobs in that language. |
| `mcp` | MCP gateway routes (baseline `quickjs` does NOT include it → MCP routes 404). | MCP work. |
| `websocket` `http_trigger` `kafka` `nats` `mqtt_trigger` `sqs_trigger` `gcp_trigger` `azure_trigger` `postgres_trigger` `native_trigger` | Each native trigger kind; none on by default (creating one 404s without its feature). | Working on / exercising that trigger. |
| `no_auth` | Treats every request as an admin superadmin (`CLOUD_HOSTED`-guarded). | Local auth-free experiments only. |
Convenience bundles (`ce`, `ee`, `oss`, …) exist in `[features]` but are heavy — prefer the
minimal explicit set for dev.
**Common combinations** (run from `backend/`):
| Goal | `--features` |
|---|---|
| Plain dev baseline (JS eval only) | `quickjs` |
| S3 / object storage / datasets (CE) | `quickjs,private,parquet` |
| S3 + EE (advanced S3 rules, on-behalf app reads, WAP, forks) | `quickjs,enterprise,private,parquet` |
| DuckLake / DuckDB (CE) | `quickjs,duckdb,parquet,private` (+ build the FFI) |
| + Python jobs | append `,python` |
## Workspace object storage in dev — use the local filesystem
For a dev workspace you don't need MinIO/S3: use the built-in **`FilesystemStorage`** large-file
storage (a root path on local disk). It is intentionally hidden from the settings-UI storage
dropdown (dev-only), so set it via the API. Requires the backend built with `parquet` (+ `private`
for the real S3 helpers, + `enterprise` if you want advanced permission rules enforced):
```bash
curl -X POST "$BASE/api/w/<ws>/workspaces/edit_large_file_storage_config" \
-H "Authorization: Bearer <admin-token>" -H "Content-Type: application/json" \
-d '{"large_file_storage":{"type":"FilesystemStorage","root_path":"/abs/writable/dir",
"public_resource":false,"advanced_permissions":null,"secondary_storage":{}}}'
```
Optional `advanced_permissions` (EE) is a list of `{"pattern":"<glob>","allow":"read[,write,delete,list]"}`
rules: admins bypass them, non-admins are confined to matching grants. Uploads/reads then flow
through the normal `job_helpers/*` (viewer-scoped) and `apps_u/*` (app-author on-behalf) S3
endpoints. Caveat: direct DuckDB access rejects filesystem stores (`"Filesystem is not supported
in DuckDB"`) — DuckLake/datatable go through the S3 proxy instead, which works.
## Running data pipelines (DuckLake) from source
DuckLake pipelines need **both** the right cargo features **and** the prebuilt DuckDB FFI. A
plain `cargo run` (or `cargo run --features quickjs`) does **not** suffice, and the failure modes
are silent-ish, so agents lose time. Verify feature names against `backend/Cargo.toml` `[features]`.
**Feature sets** (run from `backend/`):
| Goal | Command |
|---|---|
| CE DuckLake (DuckDB scripts + S3 proxy) | `cargo run --features quickjs,duckdb,parquet,private` |
| + Python scripts | add `,python` |
| EE features (WAP, partitioning, forks, …) | add `,enterprise,license` |
`enterprise` already pulls in `license`, but list both when you want the license-gated paths.
`quickjs` is for JS eval, not DuckLake per se — keep it if your baseline build had it.
**Before running any DuckDB script**, build the FFI (see the bullet above):
`cd backend/windmill-duckdb-ffi-internal && ./build_dev.sh`.
**Two gotchas that a wrong feature set produces:**
1. **`duckdb` tag advertised, feature missing.** The `duckdb` worker tag is in the *unconditional*
default tag list (`windmill-common/src/worker.rs`, `DEFAULT_TAGS`), so a worker advertises it even
without the `duckdb` feature. Jobs then dispatch but fail at execution with
`"Duck DB requires the duckdb feature to be enabled"` (`windmill-worker/src/worker.rs`). Fix:
compile with `--features duckdb`.
2. **DuckLake writes 404 (no S3 proxy).** The workspace S3 proxy (`/w/{ws}/s3_proxy/*`) that
DuckLake uses for reads/writes only mounts the real service under
`#[cfg(all(feature = "private", feature = "parquet"))]` (`windmill-api/src/s3_proxy_oss.rs`);
otherwise it's an empty router and every proxied request 404s. Fix: compile with **both**
`private` and `parquet`.
## Cloud vs self-hosted gating
The `cloud` cargo feature is compiled into **all** EE builds, so `#[cfg(feature = "cloud")]` is **not** a "cloud-only" runtime gate — it only means the code is present. The real gate for behavior specific to the managed cloud (app.windmill.dev) is the runtime flag `*CLOUD_HOSTED` (`windmill_common::worker::CLOUD_HOSTED`, from the `CLOUD_HOSTED` env var; note it's loaded from `.env` via `dotenv`, so it won't show in `/proc/<pid>/environ` — check the running behavior, not the exec env).
Cloud-only logic must be behind `if *CLOUD_HOSTED { ... }`: feature-gate the helper so it compiles, then **runtime-gate the call**. `#[cfg(feature = "cloud")]` on its own is only sufficient for:
- pure helper/struct definitions (they only run when a gated caller invokes them),
- code already inside an `if *CLOUD_HOSTED { ... }` block,
- handlers that early-return on `!*CLOUD_HOSTED`,
- idempotent no-ops that are harmless off-cloud (e.g. cache invalidation).
## Verifying Backend Changes
`cargo check` and the unit tests do not exercise a worker code path. **If you changed how
a job runs — an executor, `handle_child`, anything spawning or reading from a
subprocess — run an actual job of that kind** and confirm it completed, then say so.
Whole classes of defect compile and unit-test clean:
- **Stack overflow from a large buffer in an async block.** An array declared across an
`.await` is baked into the future's state; once that future is boxed a few layers deep
by the job poller, two 16 KB arrays abort the worker *process* (`thread
'tokio-runtime-worker' has overflowed its stack`). Heap-allocate read buffers
(`vec![0u8; N]`, not `[0u8; N]`).
- Deadlocks from draining only one of a child's pipes, missed cancellation or timeout
propagation, and anything depending on the real engine's output format.
A crash like this takes down every job on that worker, not just yours, so check the
backend log after the run rather than only the job's own status. If you cannot run one,
say which path went unexercised instead of implying it was verified.
### How many jobs a worker runs at once
`NUM_WORKERS > 1` falls back to 1 outside native mode (`backend/src/main.rs`, unless
`I_ACK_NUM_WORKERS_IS_UNSAFE`), so a worker serving script tags — `go`, `python3`,
`dependency`, `flow`, … — runs one job at a time: a per-job resource budget (memory, CPU,
temp space) shares the worker with the worker process alone.
Native mode is the exception, and budgets for its tags must divide by its concurrency:
it forces 8 workers, and `NATIVE_TAGS` includes executors that already claim a per-job
share of the worker's memory (`postgresql` and `mysql` through `MAX_SQL_RESULT_SIZE`), so
up to 8 of those run against the same limit at once.