feat: dblab provisioning module + env-create honesty gate (#127) - #128
feat: dblab provisioning module + env-create honesty gate (#127)#128samo-agent wants to merge 9 commits into
Conversation
Pins the expected behaviour before any implementation exists: - dblab-module.test.ts: asserts the full cloud-init output installs Docker CE + zfsutils, creates sparse ZFS pool, runs dblab_server 4.1.3 on 127.0.0.1:2345, caps ARC at 256 MB, sets clone cap 2 + 128 MB shared_buffers + 1 GB memory limit, creates 2 GB swapfile, and CRITICALLY never disables fail2ban or drops the UFW SSH rule while keeping the control-plane IP in fail2ban ignoreip. - dblab-env-gate.test.ts: asserts env-create for db=dblab fails LOUD (exit 1) and writes NO env record when the dblab engine is BLOCKED or UNKNOWN, and proceeds normally when READY. Both files fail for the right reasons: dblab.ts does not exist yet (module import error) and the gate logic is absent from runEnvCreate. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Implements the minimal verified €0 dblab profile from #127 into the samohost cloud-init provisioning pipeline. ## What it installs (cloud-init runcmd, in order) - Docker CE from the official apt repo + containerd.io; adminUser added to the docker group. - Docker images pre-pulled: dblab-server:4.1.3 + extended-postgres:16-0.6.2. - 2 GB swapfile at /swapfile (/etc/fstab persisted). - ZFS ARC cap: /etc/modprobe.d/zfs.conf → options zfs zfs_arc_max=268435456 (256 MB); also applied live to /sys/module/zfs/parameters/zfs_arc_max and baked into initrd via update-initramfs. - Sparse 10G loopback file at /var/lib/dblab-pool/dblab.img; attached to /dev/loop0; zpool create dblab; dataset dblab/dblab_pool with compression=on atime=off logbias=throughput. - /etc/systemd/system/dblab-loopback.service (Before=zfs-import.target docker.service) re-attaches the loop device on every boot. - /root/.dblab/engine/configs/server.yml: port 2345, enableTelemetry=false, maxCloneCount=2, maxIdleMinutes=20160, cloneAccessAddresses=127.0.0.1, portPool 6000-6099, containerResources.memory=1073741824, shared_buffers 128MB, source=host PG via Docker bridge 172.17.0.1:5432, logical retrieval jobs (logicalDump/logicalRestore/logicalSnapshot), skipStartRefresh=true. - /root/.dblab/engine/startup.sh: busybox-grep workaround (apk add grep). - Verification token generated at runtime (openssl rand -hex 32, stored at /root/.dblab/token 0600, substituted into server.yml). - pg_hba.conf entry for 172.17.0.0/16 (Docker bridge) + listen_addresses patch; postgresql reloaded. - docker run dblab_server: 127.0.0.1:2345:2345, --privileged, rshared, --restart on-failure, startup.sh. - CLI installed via dblab.sh (DBLAB_CLI_VERSION=4.1.3); symlinked into ~/bin/dblab; PATH exported in /root/.bashrc. - CLI inited: dblab init --environment-id solo. - Waits up to 60s for healthz, then verifies it. - Initial refresh triggered (dblab instance snapshots refresh). - systemctl mask ssh.socket (prevent Ubuntu 24.04 socket-activation loop). ## fail2ban preserved (#127 critical lesson) The module NEVER emits any fail2ban disable/stop command or ufw delete. The hardening module (always rendered first) keeps fail2ban enabled and writes jail.local with the control-plane IP in ignoreip via trustedIps. The combined output keeps fail2ban enabled with the whitelisted IP. ## Module registration resolveModules now has a MODULE_REGISTRY map. "dblab" → dblabModule. Unknown module names still return an error (back-compat). ## env-create honesty gate EnvExecDeps gains optional dblabPreflight?: (vm) => Promise<"READY"|"BLOCKED"|"UNKNOWN">. runEnvCreate gates on it when target.dbBackend === "dblab": - BLOCKED/UNKNOWN → exit 1, clear message with VM name and port, NO record written. - preflight throws → exit 1, exception detail in the message, NO record written. - READY → proceeds as before. - dep absent → back-compat, no gate (all existing tests unaffected). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Asserts the root-caused bugs from samograph's dead-engine incident (task wdgv5zs85) against the module before any fix is applied: 1. server.yml MUST have poolManager.preSnapshotSuffix: "_pre" Without it DBLab runs "zfs list | grep -v ''" — BusyBox grep rejects an empty -v pattern (exit 2) → "no available pools" → zero clones. 2. poolManager MUST use selectedPool: (correct yaml struct tag, not pool:) 3. logicalDump source dbname MUST be config-driven via makeDblabModule() factory — preview envs need <app>_template, not hardcoded _prod. 4. defaultEnvExecDeps() MUST export a real dblabPreflight function (currently absent → env-create honesty gate is inert in prod). All 10 tests fail (RED) against the current PR-head. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…onfig-driven sourceDb, preflight wired to prod (#128) Root cause (samograph dead-engine incident, task wdgv5zs85): The generated server.yml was missing poolManager.preSnapshotSuffix. Without it DBLab runs "zfs list ... | grep -v ''" — BusyBox grep (Alpine base image) rejects an empty -v pattern with exit 2 → "no available pools" → the engine cannot see the ZFS pool → zero clones on first boot. Fix 1 (CRITICAL): Add poolManager.preSnapshotSuffix: "_pre" to the YAML template generated by buildServerYml(). Converts "grep -v ''" (exit 2, BusyBox rejects empty pattern) into "grep -v '_pre'" (works on all greps). Fix 2: Change pool: → selectedPool: in poolManager. The old key was silently ignored by DBLab; the correct yaml struct tag is selectedPool. Harmless but correct per the v4.1.3 schema. Fix 3: Source DB is now config-driven via makeDblabModule({ sourceDb }). Preview envs need <app>_template (not <app>_prod) as the retrieval source. The new makeDblabModule() factory bakes the correct DB name into server.yml at provisioning time. dblabModule keeps a ${DBLAB_SOURCE_DB} placeholder for back-compat (operator fills it in manually). Fix 4: Wire dblabPreflight into defaultEnvExecDeps() so the env-create honesty gate fires in production. Previously dblabPreflight was absent from defaultEnvExecDeps() — the gate was silently skipped in prod, allowing fake dblab env records to be written against a non-running engine (samograph's broken path). The production implementation runs the full DBLAB_PROBES audit script over a pinned SSH connection and reuses evaluateDblabPreflight() for the verdict so the gate and `env preflight` cannot diverge. All 10 RED tests now pass GREEN. 89 dblab/preflight/gate tests pass total. Pre-existing smol-toml dependency errors in this test environment are unaffected. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
DBLab provisioning fixes — 4 root-cause bugs addressed (#128)Reference: samograph dead-engine root cause (task wdgv5zs85) Commits added
Fix 1 (CRITICAL):
|
Add test/dblab-schema-yaml-128.test.ts with js-yaml-powered structural
assertions to catch the 4 bugs found in the live smoke-provision:
BUG-1 cloneAccessAddresses emitted as sequence instead of scalar
BUG-2a skipStartRefresh at retrieval.spec (wrong nesting)
BUG-2b jobs list at retrieval.spec.jobs (wrong level)
BUG-3 losetup hardcodes /dev/loop0
BUG-4 ${DBLAB_SOURCE_USER} never substituted
All 14 new assertions fail RED on current PR head — toContain() string
checks in the old tests let invalid YAML structure through undetected.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…128 Root cause: smoke-provision on a real VM produced "[FATAL] failed to parse config" because the generated server.yml had 4 structural bugs vs the known- good samograph config (ssh -p 2223 samo@116.203.249.135). Fixes applied: BUG-1 provision.cloneAccessAddresses: scalar "127.0.0.1" (was YAML sequence) BUG-2a retrieval.refresh.skipStartRefresh (was retrieval.spec.skipStartRefresh) BUG-2b retrieval.jobs: [logicalDump, logicalRestore, logicalSnapshot] flat list under retrieval.jobs (was inline-options objects under retrieval.spec.jobs) job options now correctly at retrieval.spec.<job>.options BUG-3 losetup -f --show picks the next free loop device dynamically; Ubuntu 24.04 + snapd occupy loop0-2 so hardcoding /dev/loop0 fails; zpool create updated to use $LOOP_DEV; dblab-loopback.service uses losetup -j check + losetup -f on re-attach BUG-4 ${DBLAB_SOURCE_USER} substituted with adminUser from ProvisionSpec at build time (passed through buildFragment → buildServerYml); leaving the literal placeholder caused silent refresh failure behind || true All 1449 tests pass; 14 new YAML-structural assertions GREEN. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Smoke-test YAML bug fixes — RED/GREENSmoke-provision on a real VM produced Commits pushed
Bugs fixedBUG-1 BUG-2
BUG-3 BUG-4 What the new test does differentlyPrevious tests used
Do NOT merge — awaiting a fresh real-VM re-smoke on the updated branch. |
CloudInitFragment has writeFiles?: and runcmd?: (optional); guard with ?? [] in test/dblab-schema-yaml-128.test.ts to pass bunx tsc --noEmit. No test logic changed; 1449 tests still pass. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…rule (#128) Extends dblab-schema-yaml-128.test.ts with two failing test suites that pin the fresh-VM provision gaps surfaced by the smoke test: GAP-1: poolManager.selectedPool must be "dblab_pool" (the dataset subdirectory name under mountDir=/var/lib/dblab) — not "dblab" (the ZFS pool name). DBLab v4 looks for /var/lib/dblab/<selectedPool>; with "dblab" it looks for /var/lib/dblab/dblab which does not exist → "no available pools" loop. Ground truth: samograph ZFS: `zfs create dblab/dblab_pool` mounted at /var/lib/dblab/dblab_pool. Three YAML-parsed assertions (exact string, not substring) ensure the false-positive `toContain("selectedPool: dblab")` is superseded. GAP-2: runcmd must contain a UFW allow rule for Docker bridge subnet (172.17.0.0/16 or 172.16.0.0/12) → clone port range 6000:6099 proto tcp. Without it UFW default-deny blocks engine health probes; 100 ports × ~3s TCP timeout = ~5 min stall → engine never becomes healthy. Ground truth: samograph rule 17 "6000:6099/tcp ALLOW IN 172.16.0.0/12". Regression guards verify fail2ban and control-plane SSH rule are untouched. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…FW Docker bridge rules (#128) Root-cause analysis (diff vs samograph 116.203.249.135 live working config): GAP-1 — selectedPool MUST be "dblab_pool" (the ZFS dataset subdirectory), not "dblab" (the ZFS pool name). DBLab v4 scans poolManager.mountDir for pool subdirectories; with "dblab" it looks for /var/lib/dblab/dblab which does not exist (dataset is mounted at /var/lib/dblab/dblab_pool) → engine logs "no available pools" and loops indefinitely. Fix: selectedPool: "dblab_pool". GAP-2 — UFW rules for Docker bridge → clone ports were entirely absent. Added two source-restricted rules (never world-open): ufw allow proto tcp from 172.17.0.0/16 to any port 6000:6099 (clone pool) ufw allow proto tcp from 172.17.0.0/16 to any port 5432 (host PG) Ground truth: samograph rule 17 "6000:6099/tcp ALLOW IN 172.16.0.0/12". Without these rules UFW default-deny causes ~5 min stall on clone probes. Additional structural gaps closed by diffing samograph server.yml: - poolManager.dataSubDir: "data" + clonesMountSubDir: "clones" (missing) - databaseContainer section (missing; needed for clone container config) - retrieval.spec.logicalDump/Restore/Snapshot options: added dockerImage, containerConfig, dumpLocation, source.type:remote (all missing; needed for the logical dump job to execute correctly) - Removed the top-level containerConfig.containerResources section (not in DBLab v4 schema; samograph does not have it) Also fixes the false-positive selectedPool assertion in dblab-fixes-128.test.ts: toContain("selectedPool: dblab") is a substring of "selectedPool: dblab_pool" and would pass regardless of value. Updated to use the quoted literal form. All 1457 tests pass. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Fresh-VM gap analysis — GAP-1 + GAP-2 closed (TDD RED→GREEN)SSH ground-truth source: samograph Gaps found by diffing module output vs samograph working config
TDD commits
Test file extended
|
… prefix consistency
Blockers fixed (verified against source):
1. routing.md — CF token attribution corrected:
- CLOUDFLARE_SAMOCAT (Token 1): removed 'dns status' from 'Used by' —
src/commands/dns.ts:81 reads CLOUDFLARE_API_TOKEN, NOT CLOUDFLARE_SAMOCAT
- CLOUDFLARE_API_TOKEN (Token 2): heading changed from '(Workers deploy only)'
to '(dns status + Workers deploy)'; explains dual-use with different scopes
(dns status needs DNS-read, wrangler needs Workers:Edit); verified against
dns.ts:81, wrangler.toml:15, setup-checklist.md "Two distinct Cloudflare tokens"
- Source citation: bare 'SPEC.md' → 'samo.team/SPEC.md (line 522 diagram)'
(samohost's own SPEC.md is 131 lines and has no routing content)
2. dblab.md — clone-registry state line corrected:
- 'infra/dblab/clone-registry.ts' does not exist in samohost (no infra/ dir)
- Correct: src/state/envs.ts class EnvStore persists to ~/.samohost/envs.json
- Verified: envs.ts docstring line 4 + defaultEnvsPath() line 28
Full audit of all 7 docs — additional inaccuracies fixed:
3. supabase.md: preview.ts 'lines 19-24' → 'line 20' (grep confirms comment at :20)
4. postgres.md: added samo.team/ prefix to all three ambiguous citations:
- prose: 'infra/cloud-init/template.ts' → 'samo.team/infra/cloud-init/template.ts'
- table: 'SPEC.md §7.1 lines 762-764' → 'samo.team/SPEC.md §7.1 lines 762-764'
- inline: '(SPEC.md lines 762-764)' → '(samo.team/SPEC.md lines 762-764)'
samohost's own SPEC.md is 131 lines; lines 762-764 only exist in samo.team/SPEC.md
5. zfs.md: 'infra/zfs/pool.ts' → 'samo.team/infra/zfs/pool.ts' in parenthetical
(the explicit Source line already had the prefix; made the inline consistent)
All src/ paths verified: 12/13 exist on main; src/cloudinit/dblab.ts correctly
noted as 'in progress in PR #128, HELD' (exists on feat/dblab-provisioning-module).
All docs/ references verified present. All samo.team/ file paths verified live.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* docs(stack): canonical stack handbook for agent onboarding Adds docs/stack/ — six component docs (postgres, dblab, supabase, zfs, routing, deploy) plus a README with read-order and non-negotiable owner policies. All facts are ground-truth verified from repos and live VM state (audit 2026-07-07); no memory-only claims. Wires the handbook into CLAUDE.md via a SAMO-STACK sync block so every agent session loads the pointer on open. Resolves the PG-version contradiction: 18 is the single documented standard; the samo.team control-plane PG 17 is documented as a known gap. Captures Supabase real state (no full stack deployed anywhere; GoTrue-only on samo.team; plain bcrypt on field-record). Documents the cx23 loopback DBLab minimal profile (verified on samograph 2026-07-07, ~EUR 0 incremental). References: ground-truth audit for #127 (SAMO client package standard). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(stack-docs): address samorev REQUEST_CHANGES accuracy blockers JOB A: rebased onto clean origin/main — no #128 code in diff (docs-only). JOB B fixes: 1. dblab.md: module registry is empty on main; provisioning module lives in HELD PR #128 (not merged). Add explicit "in progress, held" note. 2. Clone image PG version: postgresai/extended-postgres:18-0.6.2 verified published on Docker Hub 2026-07-07 (resolves runbook's earlier UNVERIFIED note). Clone image is PG18. No PG16 mismatch. 3. Broken citations: src/commands/bootstrap.ts -> src/app/bootstrap.ts in postgres.md and routing.md. All src/ refs now resolve on clean main. 4. cx23 minimal profile caveat: added to both zfs.md and dblab.md — measured + proven on samograph but NOT yet formally blessed by Nik (his runbook targets 8 GB+ VMs); pending owner sign-off before it is the standard baseline. Reframed "100x over-provisioned" as measured comparison. 5. ANTHROPIC_API_KEY unconditionally banned in supabase.md (removed the conditional "unless Supabase enabled" qualifier). routing.md heading updated from "two required" to "three in total". zfs.md pool.ts source corrected: pool.ts is parameterized; template.ts line 503 hardcodes tank. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(stack-docs): full-audit accuracy pass — CF token, clone-registry, prefix consistency Blockers fixed (verified against source): 1. routing.md — CF token attribution corrected: - CLOUDFLARE_SAMOCAT (Token 1): removed 'dns status' from 'Used by' — src/commands/dns.ts:81 reads CLOUDFLARE_API_TOKEN, NOT CLOUDFLARE_SAMOCAT - CLOUDFLARE_API_TOKEN (Token 2): heading changed from '(Workers deploy only)' to '(dns status + Workers deploy)'; explains dual-use with different scopes (dns status needs DNS-read, wrangler needs Workers:Edit); verified against dns.ts:81, wrangler.toml:15, setup-checklist.md "Two distinct Cloudflare tokens" - Source citation: bare 'SPEC.md' → 'samo.team/SPEC.md (line 522 diagram)' (samohost's own SPEC.md is 131 lines and has no routing content) 2. dblab.md — clone-registry state line corrected: - 'infra/dblab/clone-registry.ts' does not exist in samohost (no infra/ dir) - Correct: src/state/envs.ts class EnvStore persists to ~/.samohost/envs.json - Verified: envs.ts docstring line 4 + defaultEnvsPath() line 28 Full audit of all 7 docs — additional inaccuracies fixed: 3. supabase.md: preview.ts 'lines 19-24' → 'line 20' (grep confirms comment at :20) 4. postgres.md: added samo.team/ prefix to all three ambiguous citations: - prose: 'infra/cloud-init/template.ts' → 'samo.team/infra/cloud-init/template.ts' - table: 'SPEC.md §7.1 lines 762-764' → 'samo.team/SPEC.md §7.1 lines 762-764' - inline: '(SPEC.md lines 762-764)' → '(samo.team/SPEC.md lines 762-764)' samohost's own SPEC.md is 131 lines; lines 762-764 only exist in samo.team/SPEC.md 5. zfs.md: 'infra/zfs/pool.ts' → 'samo.team/infra/zfs/pool.ts' in parenthetical (the explicit Source line already had the prefix; made the inline consistent) All src/ paths verified: 12/13 exist on main; src/cloudinit/dblab.ts correctly noted as 'in progress in PR #128, HELD' (exists on feat/dblab-provisioning-module). All docs/ references verified present. All samo.team/ file paths verified live. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: samo-agent <samo-agent@users.noreply.github.com> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
… VMs Root cause: `dblab-loopback.service` (a per-VM artifact, not in samohost provisioning) declared `Before=zfs-import.target` but `zfs-import-cache.service` started independently before the loopback was set up, causing it to abort (signal=ABRT) — leaving the ZFS pool `dblab` unmounted on every reboot. Manual workaround (operator steps): 1. Rewrite `dblab-loopback.service` to explicitly declare `Before=zfs-import-cache.service` (not just `zfs-import.target`). 2. Add a drop-in `/etc/systemd/system/zfs-import-cache.service.d/10-dblab-loopback.conf` with `Requires=dblab-loopback.service` + `After=dblab-loopback.service`. This makes the ZFS import wait for the loop device to exist. Applied live on samograph VM 2026-07-16. Bug B is NOT a samohost-code bug — the unit is a per-VM artifact installed manually per the runbook. The platform gap (DBLab not auto-provisioned by cloud-init) is tracked in docs/stack/dblab.md. The fix must be baked into the future cloud-init module (PR #128 follow-up). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… boot docs (#176) (#176) * test(port-check): RED — same-env sibling/failed unit must not be foreign squatter Reproduces Bug A: for multi-service apps the port-check calls `samohost_port_check_ok <port> <listener-unit>@<env>.service` for each listener. When the listener's unit is FAILED but the port is held by a sibling unit of the same env, `systemctl is-active` returns false and the check misclassifies the port as a foreign squatter, triggering `rm -f SAMOHOST_CADDY_SNIPPET` + Caddy reload — killing the preview vhost. Observed on samograph VM (2026-07-16): samograph-docs-add-robot-to-readme and other envs with all ports = 3111 (single-port allocation); the web@env unit holds 3111 while live@env unit fails with EADDRINUSE, and heal cycles delete the Caddy snippet on every pass (CF 525). New tests (f1–f7): - f1: sibling same-env unit in cgroup → must return 0 (own, not foreign) - f2: same unit in FAILED state → cgroup match → must return 0 - f3: foreign process cgroup → must reject (non-zero) - f4: generated function must declare env_name param - f5: generated function must call samohost_read_cgroup_for_pid - f6: call site must pass env name as 3rd arg - f7: is-active fast path must be preserved (backward compat) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(port-check): cgroup-based same-env ownership — failed sibling unit no longer classified as foreign squatter (Bug A) Root cause: `samohost_port_check_ok(port, unit)` used `systemctl is-active` as the sole ownership signal. For multi-service apps (samograph: web + live services), when `samograph-live@env.service` is FAILED but a sibling unit `samograph-web@env.service` holds the same port (both got port=3111 in a single-port allocation), the check returns false-positive "foreign squatter" → `rm -f SAMOHOST_CADDY_SNIPPET` + `caddy reload` → CF 525 on every heal cycle. Fix: 1. Add `samohost_read_cgroup_for_pid()` helper (reads /proc/<pid>/cgroup, stubbed in tests without real /proc access). 2. Extend `samohost_port_check_ok` with a third `env_name` argument. After the `is-active` fast path fails, iterate PIDs holding the port via `ss -tlnHp` and check `/proc/<pid>/cgroup` for `@<envName>.service`. Any process whose cgroup matches our env's suffix is classified as OURS (not foreign) — covers both: a failed same-env unit being restarted, and a sibling service that shares the same port value. 3. Update all call sites to pass `sq(t.name)` as the third argument. Backward-compat: `systemctl is-active` fast path preserved; `env_name` defaults to "" so callers that omit it get the old behaviour. Verified: 2056/2056 tests pass (0 regressions). Real-world: samograph VM had samograph-docs-add-robot-to-readme and samograph-docs-polish-user-facing-text vhosts deleted on every heal cycle; fix prevents the deletion path. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * docs(dblab): document Bug B — loopback ZFS boot ordering fix for cx23 VMs Root cause: `dblab-loopback.service` (a per-VM artifact, not in samohost provisioning) declared `Before=zfs-import.target` but `zfs-import-cache.service` started independently before the loopback was set up, causing it to abort (signal=ABRT) — leaving the ZFS pool `dblab` unmounted on every reboot. Manual workaround (operator steps): 1. Rewrite `dblab-loopback.service` to explicitly declare `Before=zfs-import-cache.service` (not just `zfs-import.target`). 2. Add a drop-in `/etc/systemd/system/zfs-import-cache.service.d/10-dblab-loopback.conf` with `Requires=dblab-loopback.service` + `After=dblab-loopback.service`. This makes the ZFS import wait for the loop device to exist. Applied live on samograph VM 2026-07-16. Bug B is NOT a samohost-code bug — the unit is a per-VM artifact installed manually per the runbook. The platform gap (DBLab not auto-provisioned by cloud-init) is tracked in docs/stack/dblab.md. The fix must be baked into the future cloud-init module (PR #128 follow-up). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(port-check): replace gawk match() with grep -oP for PID extraction; tighten f6 env-name assertion samorev REQUEST_CHANGES — two blocking fixes: 1. awk portability (finding 1): the 3-arg match(\$0, /pid=([0-9]+)/, a) form is a gawk extension. On mawk (Ubuntu's default awk alternative when gawk is not installed, which it is not in the bootstrap list), it errors silently: zero PIDs are extracted, the cgroup ownership check is skipped entirely, and the function falls through to return 1 (foreign squatter) — reproducing the exact bug this PR fixes. Replaced with: ss -tlnHp | grep -F ":${port} " | grep -oP 'pid=\K[0-9]+' GNU grep is always present on Ubuntu and the -oP / \K forms work correctly. Added integration test f8 that runs the grep fragment against the host's actual grep with a synthetic ss line and asserts both PIDs are extracted. f8 also asserts the gawk 3-arg pattern is NOT present in the generated fn. 2. f6 too weak (finding 2): the old regex matched any 3-token call. New test extracts every samohost_port_check_ok call line from the script, strips quoting from the third positional token, and compares it to target().name ("field-record-1-feat-x") exactly. A blank, wrong, or missing env name would now fail the test. Also added inline comment at the ss -tlnHp block noting the grep -oP portability rationale (non-blocking suggestion from samorev). 2057/2057 tests pass. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: samo-agent <samo-agent@samohost.dev> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Summary
Closes the gap from #127: SAMO clients are now dblab-ready by default at provision time. The hardening-only cloud-init now gains an optional
dblabmodule that provisions the full minimal €0 profile verified in #127.src/cloudinit/dblab.ts— newModuleimplementation. Generates a deterministic cloud-init fragment that:dblab) on the root disk with a systemd persistence service./etc/modprobe.d/zfs.conf(applied live + baked into initrd)./etc/fstab./root/.dblab/engine/configs/server.ymlwith the minimal profile: port 2345,maxCloneCount=2,maxIdleMinutes=20160,cloneAccessAddresses=127.0.0.1, portPool 6000–6099,containerResources.memory=1073741824(1 GB),shared_buffers=128MB, source = host PG via Docker bridge172.17.0.1:5432, logical retrieval jobs.pg_hba.confso the engine can dump prod Postgres.dblab_server:4.1.3on127.0.0.1:2345only (--privileged,--restart on-failure,rshared).ssh.socketto prevent the Ubuntu 24.04 restart loop.src/commands/preview.ts—resolveModulesnow has aMODULE_REGISTRYmap."dblab"resolves todblabModule. Unknown names still error.src/commands/env.ts—EnvExecDepsgains optionaldblabPreflight?.runEnvCreategates on it whendb=dblab:BLOCKED/UNKNOWN/throw → exit 1 + clear error, no env record written (the samograph broken path: fake record written, clone never existed).Root-cause analysis (why the gate matters)
samograph's manual dblab install wrote env records without verifying the engine was healthy. The reconciler saw
needDeploy=falseon those records and never retried the broken env. The gate is inserted BEFORE the DNS step and BEFORE any record write, so the only way an env record exists is if the engine was confirmed READY at create time.fail2ban / control-plane exemption
The
dblabModulecloud-init fragment contains zero fail2ban or UFW commands. The hardening module (always rendered first) writesjail.localwithtrustedIpsinignoreip;provision.tsauto-injects the control-plane egress IP intotrustedIpsbefore rendering. The combined output keeps fail2ban enabled and the control-plane whitelisted.Tested assertions:
never toContain("systemctl stop fail2ban")never toContain("systemctl disable fail2ban")never toMatch(/systemctl\s+(stop|disable)\s+fail2ban/)never toContain("ufw delete")toContain("systemctl enable --now fail2ban")(from hardening module)toContain("ignoreip")+toContain("91.99.233.145")within 200 charsTDD
RED commit
a03f09f: 2 test files, 0 source changes, CI red on missing module import + missing gate.GREEN commit
79a0e45: implementation, 1423/1423 tests pass, tsc clean.What real-provision smoke test is still needed post-merge
--module dblaband walk the 19 steps from SAMO client package: every client gets prod + dblab previews + auto-deploy #127 manually on the live VM.curl http://127.0.0.1:2345/healthzreturns{"edition":"community",...}.samohost env preflight <vm>→ READY.samohost env create <vm> <app> --branch smoke --db dblab→ exit 0 + env record written.db=none) need the module applied and a first refresh with their prod database name substituted intoserver.yml.Not in this PR (deferred, filed confidentially)
#124findVm ignores lifecycleState (pre-existing)#125trigger swallows preview failures (pre-existing)dblabPreflightindefaultEnvExecDeps()(requires the healthz curl probe via SSH runner — low-risk follow-up once the module is deployed and smoke-tested)Closes #127 provisioning checklist item.
🤖 Generated with Claude Code