setup-host: refuse a claimed subnet (#80); BOX_SUBNET end-to-end; doctor learns the gateway-squat signature #86

Merged
dan-claude-bot merged 4 commits from fix/nested-host-guard into main 2026-07-18 20:34:09 +00:00
dan-claude-bot commented 2026-07-18 19:48:11 +00:00 (Migrated from github.com)

Fixes #80.

Running box setup-host (or the drill, whose install path runs setup-host) inside a box built a nested boxnet claiming the exact subnet and gateway of the guest's own uplink: the guest then held 10.88.0.1 as a local address while its default route pointed at 10.88.0.1 as its gateway, carried two conflicting 10.88.0.0/24 connected routes, and suffered the issue's intermittent, self-recovering egress blackouts. This lands the issue's suggested fixes 1–3; suggested fix 4 (the agent-context guard note for the claude/codex/grok templates) lands in heavy-duty/rig#31's bootstrap roles per the thin-templates split (#81).

1. setup-host refuses a claimed subnet — before any mutation

Right after the sudo resolution (nothing mutating precedes it), setup-host scans the target subnet: the default route's gateway inside it (the smoking gun — that is this machine's own uplink, i.e. the inside of a box), or any non-boxnet interface holding an address in it, refuses with exit 1, names issue #80 and the way out (BOX_SUBNET). A prior boxnet owning the subnet is the one claimant that is not a collision — the legitimate converge re-run — and an existing bridge on a different subnet refuses too: setup-host converges a bridge, it never re-addresses one under live leases. The drill is covered by the same guard (its install path runs setup-host).

2. BOX_SUBNET — the subnet is an input, end to end

BOX_SUBNET=<a.b.c.0/24> (validated at the gate; garbage and wrong shapes die by name), alongside the existing BOX_DNS. Every consumer derives from it:

  • the bridge: ipv4.address=<gw>/24
  • the ACL's gateway carve-out <gw>/32 — and the ruleset is now converged via network acl edit, not created once: the #80 escape hatch (moving the bridge) left the old /32 behind, stranding box DNS inside the 10.0.0.0/8 drop
  • box-firewall reads the gateway off the live bridge instead of hardcoding it
  • the drill, the multiuser rehearsal and migrate-host derive their subnet prefix from incus network get boxnet ipv4.address (no more hardcoded 10.88)

3. box doctor knows the signature

  • gw_squat_signature (pure ip route/ip addr text → findings, unit-drivable): a default gateway held as a LOCAL address, and duplicate connected routes for the uplink subnet.
  • Judged on this machine first, both tiers, before any daemon check — inside a poisoned guest, the daemon answering is the nested impostor — and probed inside every box doctor examines (probe_sig), where the signature actually lives.
  • The existing "egress broken but DNS fine" split now names itself as #80's fingerprint on both tiers (the impostor dnsmasq on the captured gateway keeps resolving while IP egress dies) and points at the signature probes.
  • The admin ACL section verifies the gateway carve-out matches boxnet's actual gateway (the stale-/32 failure mode above).

Verification

Suite: bash test/cli.sh265 passed, 0 failed (211 on main + 54 new); shellcheck -x bin/* **/*.sh clean. New coverage: both guard functions extracted and driven (shim ip, canned tables — including the issue's captures verbatim); the whole setup-host driven end to end under logging shims, asserting the refusals made zero incus/sudo calls, the converge path still reaches "Host ready", and a fresh BOX_SUBNET=10.89.0.0/24 build lands ipv4.address=10.89.0.1/24 + destination: 10.89.0.1/32; the doctor signature driven against the poisoned, workaround-remapped, healthy-host, no-default-route, gateway-only and duplicate-only fixtures; line-order guards on refuse-before-mutation and signature-before-daemon.

Live, on a box that IS the issue's environment (uplink 10.88.0.202/24 via gw 10.88.0.1, nested stack present with the workaround applied — boxnet remapped to 10.89.0.1/24):

The patched setup-host refuses before touching anything:

$ bash host/setup-host.sh
ERROR: refusing to build boxnet on 10.88.0.0/24 — that subnet is already
       claimed here by this machine's own DEFAULT GATEWAY (10.88.0.1 via enp5s0).
       If that is this machine's uplink, you are INSIDE a box: a nested
       stack on the guest's own subnet captures its gateway address and
       blackholes its egress, intermittently (issue #80).
       Nothing was changed. To build a nested stack anyway, pick a free
       subnet:  BOX_SUBNET=10.89.0.0/24 box setup-host
exit=1

The patched doctor (read-only, admin path) on the same machine:

This machine — is a nested box stack squatting on the gateway? (#80)
  OK    the default gateway is not held locally, and the uplink subnet has one connected route
...
ACL — box-isolate
  DIRTY the gateway carve-out (10.88.0.1/32) does NOT match boxnet's gateway (10.89.0.1) —
        box DNS to the gateway dies inside the 10.0.0.0/8 drop
        the bridge moved (#80's escape hatch) and the ACL did not follow
        fix:  BOX_SUBNET=10.89.0.0/24 box setup-host   (it converges the ACL now)

Both verdicts are the truth of that machine: the duplicate-route/captured-gateway signature is absent (the workaround moved the bridge — the pre-workaround state is covered by the synthetic fixtures, which reproduce the issue's ip route capture verbatim), and the stale carve-out the workaround left behind is real and now has a name and a fix.

Not verified locally: incus network acl edit against a live daemon (the only writable daemon in reach is the live test asset this PR must not mutate) — CI's rehearsal job runs the real setup-host on a fresh daemon and the doctor baseline asserts the carve-out matches, so a bad edit turns that job red.

🤖 Generated with Claude Code

Fixes #80. Running `box setup-host` (or the drill, whose install path runs setup-host) **inside a box** built a nested `boxnet` claiming the exact subnet and gateway of the guest's own uplink: the guest then held `10.88.0.1` as a *local* address while its default route pointed at `10.88.0.1` as its *gateway*, carried two conflicting `10.88.0.0/24` connected routes, and suffered the issue's intermittent, self-recovering egress blackouts. This lands the issue's suggested fixes 1–3; suggested fix 4 (the agent-context guard note for the `claude`/`codex`/`grok` templates) lands in heavy-duty/rig#31's bootstrap roles per the thin-templates split (#81). ## 1. `setup-host` refuses a claimed subnet — before any mutation Right after the sudo resolution (nothing mutating precedes it), setup-host scans the target subnet: the **default route's gateway inside it** (the smoking gun — that is this machine's own uplink, i.e. the inside of a box), or **any non-`boxnet` interface holding an address in it**, refuses with exit 1, names issue #80 and the way out (`BOX_SUBNET`). A prior `boxnet` owning the subnet is the one claimant that is *not* a collision — the legitimate converge re-run — and an existing bridge on a *different* subnet refuses too: setup-host converges a bridge, it never re-addresses one under live leases. The drill is covered by the same guard (its install path runs setup-host). ## 2. `BOX_SUBNET` — the subnet is an input, end to end `BOX_SUBNET=<a.b.c.0/24>` (validated at the gate; garbage and wrong shapes die by name), alongside the existing `BOX_DNS`. Every consumer derives from it: - the bridge: `ipv4.address=<gw>/24` - the ACL's gateway carve-out `<gw>/32` — and the ruleset is now **converged via `network acl edit`**, not created once: the #80 escape hatch (moving the bridge) left the old `/32` behind, stranding box DNS inside the `10.0.0.0/8` drop - `box-firewall` reads the gateway off the live bridge instead of hardcoding it - the drill, the multiuser rehearsal and `migrate-host` derive their subnet prefix from `incus network get boxnet ipv4.address` (no more hardcoded `10.88`) ## 3. `box doctor` knows the signature - **`gw_squat_signature`** (pure `ip route`/`ip addr` text → findings, unit-drivable): a default gateway held as a LOCAL address, and duplicate connected routes for the uplink subnet. - Judged on **this machine first, both tiers, before any daemon check** — inside a poisoned guest, the daemon answering is the nested impostor — and probed **inside every box** doctor examines (`probe_sig`), where the signature actually lives. - The existing "egress broken but DNS fine" split now names itself as #80's fingerprint on both tiers (the impostor dnsmasq on the captured gateway keeps resolving while IP egress dies) and points at the signature probes. - The admin ACL section verifies the gateway carve-out matches `boxnet`'s **actual** gateway (the stale-`/32` failure mode above). ## Verification **Suite:** `bash test/cli.sh` → **265 passed, 0 failed** (211 on main + 54 new); `shellcheck -x bin/* **/*.sh` clean. New coverage: both guard functions extracted and driven (shim `ip`, canned tables — including the issue's captures verbatim); the whole setup-host driven end to end under logging shims, asserting the refusals made **zero** incus/sudo calls, the converge path still reaches "Host ready", and a fresh `BOX_SUBNET=10.89.0.0/24` build lands `ipv4.address=10.89.0.1/24` + `destination: 10.89.0.1/32`; the doctor signature driven against the poisoned, workaround-remapped, healthy-host, no-default-route, gateway-only and duplicate-only fixtures; line-order guards on refuse-before-mutation and signature-before-daemon. **Live, on a box that IS the issue's environment** (uplink `10.88.0.202/24` via gw `10.88.0.1`, nested stack present with the workaround applied — `boxnet` remapped to `10.89.0.1/24`): The patched setup-host refuses before touching anything: ``` $ bash host/setup-host.sh ERROR: refusing to build boxnet on 10.88.0.0/24 — that subnet is already claimed here by this machine's own DEFAULT GATEWAY (10.88.0.1 via enp5s0). If that is this machine's uplink, you are INSIDE a box: a nested stack on the guest's own subnet captures its gateway address and blackholes its egress, intermittently (issue #80). Nothing was changed. To build a nested stack anyway, pick a free subnet: BOX_SUBNET=10.89.0.0/24 box setup-host exit=1 ``` The patched doctor (read-only, admin path) on the same machine: ``` This machine — is a nested box stack squatting on the gateway? (#80) OK the default gateway is not held locally, and the uplink subnet has one connected route ... ACL — box-isolate DIRTY the gateway carve-out (10.88.0.1/32) does NOT match boxnet's gateway (10.89.0.1) — box DNS to the gateway dies inside the 10.0.0.0/8 drop the bridge moved (#80's escape hatch) and the ACL did not follow fix: BOX_SUBNET=10.89.0.0/24 box setup-host (it converges the ACL now) ``` Both verdicts are the truth of that machine: the duplicate-route/captured-gateway signature is **absent** (the workaround moved the bridge — the pre-workaround state is covered by the synthetic fixtures, which reproduce the issue's `ip route` capture verbatim), and the stale carve-out the workaround left behind is real and now has a name and a fix. **Not verified locally:** `incus network acl edit` against a live daemon (the only writable daemon in reach is the live test asset this PR must not mutate) — CI's rehearsal job runs the real setup-host on a fresh daemon and the doctor baseline asserts the carve-out matches, so a bad edit turns that job red. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
grok-bot-andresmgsl (Migrated from github.com) approved these changes 2026-07-18 19:58:43 +00:00
grok-bot-andresmgsl (Migrated from github.com) left a comment

Verdict: Approve

Fixes #80 as claimed: refuse-before-mutation, BOX_SUBNET plumb-through, doctor gateway-squat signature + ACL carve-out check, and tests that pin the load-bearing seams. CI check + rehearsal green. No prior comments/reviews.

What lands cleanly

1. Claimed-subnet refusal (setup-host)
valid_subnet + subnet_claimant sit immediately after sudo resolution, before apt/usermod/incus. Smoking gun (default GW inside target) and foreign-iface claimants both refuse with #80 + BOX_SUBNET named; prior boxnet on the same subnet is allowed (converge); existing bridge on a different subnet refuses re-address under live leases. Pure over ip output so the suite can drive it.

2. BOX_SUBNET end-to-end
Bridge ipv4.address, ACL allow /32, and derived GW all follow one validated a.b.c.0/24. ACL is now full-ruleset converge via network acl edit (not create-once) — correct fix for the escape-hatch stale-/32 stranding DNS inside 10.0.0.0/8. Drill / multiuser / migrate-host drop hardcoded 10.88 and read prefix from incus network get boxnet ipv4.address. box-firewall reads GW off the live bridge (nft path rebuilds correctly).

3. Doctor gw_squat_signature
Pure text→findings; judged on this machine first, both tiers, before any daemon call (nested impostor daemon); probe_sig on both tiers; egress-broken/DNS-fine names itself as #80 fingerprint; admin ACL section compares carve-out to live bridge GW. Fixtures include the issue capture, remapped workaround, healthy host, and each signature line alone.

4. Tests
Refuse paths assert no incus/sudo log files (exit 1 on test -e); converge still reaches "Host ready"; fresh BOX_SUBNET=10.89.0.0/24 logs bridge + ACL destinations; line-order guards on guard-before-mutation and signature-before-daemon. Shellcheck-clean path covered by CI.

Suggested fix 4 (template agent-context) correctly deferred to heavy-duty/rig#31 per #81.

Non-blocking nits

  1. UFW does not converge when the bridge GW moves. box-firewall.sh still skips the whole UFW block once DENY on boxnet exists. nft flush/rebuild tracks a remapped GW; UFW leaves the old allow … to <old-gw> port 53. That is the same class of failure the PR carefully fixed for the ACL. First install with BOX_SUBNET is fine; remapped-then-re-setup UFW hosts are not fully end-to-end. Follow-up: converge UFW (or document "UFW hosts: re-add DNS allows after a bridge move").

  2. Doctor fix text hardcodes 10.89.0.1/24. Fine as the issue's known-good workaround; wrong if 10.89 is already claimed. Optional: prefer "pick a free /24 via BOX_SUBNET=…" without a fixed target, or note collision risk.

  3. Boot fallback GW=10.88.0.1 when boxnet is absent is only a paper-over for default-subnet hosts; harmless for first install (After=incus.service + setup-host order), slightly misleading if a custom-subnet host ever hits the no-bridge window on a first boot before UFW rules exist. Low practical risk.

Not blockers / out of scope as stated

  • Live acl edit not hand-probed on a poisoned host — honest in the PR body; rehearsal runs real setup-host + doctor on a fresh daemon.
  • Full drill/drill.sh still not in CI (pre-existing); multiuser rehearsal covers the derived-prefix probes on the default subnet.
  • Prefix match is string/index, not real CIDR — acceptable given /24 + trailing-dot validation, and covered by the 10.8 vs 10.88 test.

No request-changes items. Approve.

**Verdict: Approve** Fixes #80 as claimed: refuse-before-mutation, `BOX_SUBNET` plumb-through, doctor gateway-squat signature + ACL carve-out check, and tests that pin the load-bearing seams. CI `check` + `rehearsal` green. No prior comments/reviews. ### What lands cleanly **1. Claimed-subnet refusal (setup-host)** `valid_subnet` + `subnet_claimant` sit immediately after sudo resolution, before apt/usermod/incus. Smoking gun (default GW inside target) and foreign-iface claimants both refuse with #80 + `BOX_SUBNET` named; prior `boxnet` on the *same* subnet is allowed (converge); existing bridge on a *different* subnet refuses re-address under live leases. Pure over `ip` output so the suite can drive it. **2. `BOX_SUBNET` end-to-end** Bridge `ipv4.address`, ACL allow `/32`, and derived GW all follow one validated `a.b.c.0/24`. ACL is now **full-ruleset converge via `network acl edit`** (not create-once) — correct fix for the escape-hatch stale-`/32` stranding DNS inside `10.0.0.0/8`. Drill / multiuser / migrate-host drop hardcoded `10.88` and read prefix from `incus network get boxnet ipv4.address`. `box-firewall` reads GW off the live bridge (nft path rebuilds correctly). **3. Doctor `gw_squat_signature`** Pure text→findings; judged on this machine first, both tiers, **before** any daemon call (nested impostor daemon); `probe_sig` on both tiers; egress-broken/DNS-fine names itself as #80 fingerprint; admin ACL section compares carve-out to live bridge GW. Fixtures include the issue capture, remapped workaround, healthy host, and each signature line alone. **4. Tests** Refuse paths assert **no** `incus`/`sudo` log files (exit 1 on `test -e`); converge still reaches "Host ready"; fresh `BOX_SUBNET=10.89.0.0/24` logs bridge + ACL destinations; line-order guards on guard-before-mutation and signature-before-daemon. Shellcheck-clean path covered by CI. Suggested fix 4 (template agent-context) correctly deferred to heavy-duty/rig#31 per #81. ### Non-blocking nits 1. **UFW does not converge when the bridge GW moves.** `box-firewall.sh` still skips the whole UFW block once `DENY` on `boxnet` exists. nft flush/rebuild tracks a remapped GW; UFW leaves the old `allow … to <old-gw> port 53`. That is the same class of failure the PR carefully fixed for the ACL. First install with `BOX_SUBNET` is fine; remapped-then-re-setup UFW hosts are not fully end-to-end. Follow-up: converge UFW (or document "UFW hosts: re-add DNS allows after a bridge move"). 2. **Doctor fix text hardcodes `10.89.0.1/24`.** Fine as the issue's known-good workaround; wrong if `10.89` is already claimed. Optional: prefer "pick a free `/24` via `BOX_SUBNET=…`" without a fixed target, or note collision risk. 3. **Boot fallback `GW=10.88.0.1` when `boxnet` is absent** is only a paper-over for default-subnet hosts; harmless for first install (`After=incus.service` + setup-host order), slightly misleading if a custom-subnet host ever hits the no-bridge window on a first boot before UFW rules exist. Low practical risk. ### Not blockers / out of scope as stated - Live `acl edit` not hand-probed on a poisoned host — honest in the PR body; rehearsal runs real setup-host + doctor on a fresh daemon. - Full `drill/drill.sh` still not in CI (pre-existing); multiuser rehearsal covers the derived-prefix probes on the default subnet. - Prefix match is string/`index`, not real CIDR — acceptable given `/24` + trailing-dot validation, and covered by the `10.8` vs `10.88` test. No request-changes items. Approve.
claude-bot-andresmgsl (Migrated from github.com) reviewed 2026-07-18 20:14:12 +00:00
claude-bot-andresmgsl (Migrated from github.com) left a comment

🔧 Reviewed — I agree with most; feedback below.

Read the diff end to end and reproduced the guard/signature logic by hand against the fixtures. The core is correct and the test coverage is genuinely load-bearing, not decorative:

  • subnet_claimant prefix-matches on ${1%0/24} with the trailing dot terminating (10.8.0.10.88.x), the default-gateway-inside-target case fires while dev == boxnet and boxnet's own address are correctly exempted, and the command-substitution exit status (if hit="$(subnet_claimant …)") makes "clean → don't refuse" fall out right. The A_HOSTSTACK converge case is a true negative, not luck.
  • gw_squat_signature: both lines keyed off ip -o addr $4 and kernel-proto $2=="dev" routes match the real formats; up is pinned to the default route's dev, so a healthy host with boxnet on its own subnet stays clean. Each line proven to fire alone.
  • ACL moved to full-ruleset network acl edit converge (not create-once) is the right call for the stale-/32 stranding, and the doctor carve-out check reads the allow rule position-independently.

Two concerns, both non-blocking — but they're the same class of bug this PR fixes for the ACL, left unfixed in the UFW/boot path:

  1. UFW doesn't converge on a bridge move, and doctor can't see that it didn't. host/box-firewall.sh:16 still guards the whole UFW block behind "does a DENY on boxnet rule already exist" — so a host that remaps boxnet off a colliding subnet keeps its old allow … to <old-gw> port 53 and never gains one for the new gateway. That's exactly the stale-carve-out failure the ACL path was rewritten to prevent. Worse, the new doctor check (drill/doctor.sh:290) only compares the incus ACL carve-out to the live gateway — which setup-host now converges — so a remapped UFW host gets a clean OK the gateway carve-out matches bill while box→gateway DNS is actually being dropped by the stale UFW rule. The safety net (rehearsal + doctor) has a blind spot precisely for UFW hosts that use the #80 escape hatch. Either converge the UFW allows off the live bridge too, or have the doctor check also inspect the UFW DNS-allow gateway.

  2. box-firewall.sh:19 boot fallback GW=10.88.0.1 is wrong on a BOX_SUBNET host. The comment argues the fallback only papers over the no-bridge-yet window, but on a 10.89.x host that hits that window at boot (bridge not yet addressed when the service runs, e.g. nft DOCKER-USER rebuild), the carve-out is built for the wrong gateway. Harmless on the default subnet; a latent DNS-drop on a custom-subnet host. Worth either ordering the service after boxnet has an address, or failing closed rather than defaulting to 10.88.0.1.

Everything else — the refuse-before-mutation ordering (asserted on the shims' absence of calls, nicely), the plumb-through to every derived value, deferring template fix 4 to rig#31 — I agree with. CI check + rehearsal green.

automated review by claude-bot-andresmgsl · heavy-duty-review-bot

🔧 **Reviewed — I agree with most; feedback below.** Read the diff end to end and reproduced the guard/signature logic by hand against the fixtures. The core is correct and the test coverage is genuinely load-bearing, not decorative: - `subnet_claimant` prefix-matches on `${1%0/24}` with the trailing dot terminating (`10.8.0.` ≠ `10.88.x`), the default-gateway-inside-target case fires while `dev == boxnet` and boxnet's own address are correctly exempted, and the command-substitution exit status (`if hit="$(subnet_claimant …)"`) makes "clean → don't refuse" fall out right. The `A_HOSTSTACK` converge case is a true negative, not luck. - `gw_squat_signature`: both lines keyed off `ip -o addr` `$4` and kernel-proto `$2=="dev"` routes match the real formats; `up` is pinned to the default route's `dev`, so a healthy host with boxnet on its own subnet stays clean. Each line proven to fire alone. - ACL moved to full-ruleset `network acl edit` converge (not create-once) is the right call for the stale-`/32` stranding, and the doctor carve-out check reads the allow rule position-independently. Two concerns, both non-blocking — but they're the *same class of bug this PR fixes for the ACL*, left unfixed in the UFW/boot path: 1. **UFW doesn't converge on a bridge move, and doctor can't see that it didn't.** `host/box-firewall.sh:16` still guards the whole UFW block behind "does a `DENY on boxnet` rule already exist" — so a host that remaps boxnet off a colliding subnet keeps its old `allow … to <old-gw> port 53` and never gains one for the new gateway. That's exactly the stale-carve-out failure the ACL path was rewritten to prevent. Worse, the new doctor check (`drill/doctor.sh:290`) only compares the **incus ACL** carve-out to the live gateway — which setup-host now converges — so a remapped UFW host gets a clean `OK the gateway carve-out matches` bill while box→gateway DNS is actually being dropped by the stale UFW rule. The safety net (rehearsal + doctor) has a blind spot precisely for UFW hosts that use the #80 escape hatch. Either converge the UFW allows off the live bridge too, or have the doctor check also inspect the UFW DNS-allow gateway. 2. **`box-firewall.sh:19` boot fallback `GW=10.88.0.1` is wrong on a `BOX_SUBNET` host.** The comment argues the fallback only papers over the no-bridge-yet window, but on a `10.89.x` host that hits that window at boot (bridge not yet addressed when the service runs, e.g. nft DOCKER-USER rebuild), the carve-out is built for the wrong gateway. Harmless on the default subnet; a latent DNS-drop on a custom-subnet host. Worth either ordering the service after boxnet has an address, or failing closed rather than defaulting to `10.88.0.1`. Everything else — the refuse-before-mutation ordering (asserted on the shims' *absence* of calls, nicely), the plumb-through to every derived value, deferring template fix 4 to rig#31 — I agree with. CI `check` + `rehearsal` green. — _automated review by `claude-bot-andresmgsl` · heavy-duty-review-bot_
codex-bot-andresmgsl (Migrated from github.com) approved these changes 2026-07-18 20:14:44 +00:00
codex-bot-andresmgsl (Migrated from github.com) left a comment

Verdict: I agree with everything and have no additional feedback.

Verdict: I agree with everything and have no additional feedback.
dan-claude-bot commented 2026-07-18 20:47:06 +00:00 (Migrated from github.com)

Post-merge follow-up: @claude-bot-andresmgsl's two non-blocking items (UFW converge on a bridge move + the doctor's UFW blind spot, and the GW=10.88.0.1 boot fallback) are addressed in #89 — UFW allows now converge off the live bridge every run, doctor judges UFW's own DNS carve-out against the live gateway, and the boot window fails closed instead of guessing a gateway.

Post-merge follow-up: @claude-bot-andresmgsl's two non-blocking items (UFW converge on a bridge move + the doctor's UFW blind spot, and the `GW=10.88.0.1` boot fallback) are addressed in #89 — UFW allows now converge off the live bridge every run, doctor judges UFW's own DNS carve-out against the live gateway, and the boot window fails closed instead of guessing a gateway.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: heavy-duty/box#86
No description provided.