Skip to content

Config: deployment-managed env values (opt-in via DAPPS_ENV_MANAGED); derive the DAPPS callsign from the host node, probing for a free SSID - #149

Merged
M0LTE merged 3 commits into
masterfrom
config-env-precedence
Jun 11, 2026
Merged

Config: deployment-managed env values (opt-in via DAPPS_ENV_MANAGED); derive the DAPPS callsign from the host node, probing for a free SSID#149
M0LTE merged 3 commits into
masterfrom
config-env-precedence

Conversation

@M0LTE

@M0LTE M0LTE commented Jun 11, 2026

Copy link
Copy Markdown
Collaborator

DAPPS now ships under pdn as a supervised app package, which made the first-start-only env seeding awkward: the host's app config should stay authoritative across restarts, and the owner shouldn't have to invent a callsign that the host already knows. Two behaviours, one PR — both completely inert for standalone installs.

1. Deployment-managed env values — opt-in via DAPPS_ENV_MANAGED=true (standalone flow untouched by default)

Review fix: the first cut re-applied set DAPPS_* vars unconditionally — a breaking change, since the shipped standalone flow (scripts/dapps.serviceEnvironmentFile=/etc/dapps.env) keeps DAPPS_* permanently set and would have had every dashboard edit reverted on every restart.

DAPPS_ENV_MANAGED is a mode switch read each start (itself never stored):

  • unset / false (the standalone default): byte-for-byte today's semantics. Env vars seed rows that don't exist yet (first start) and nothing else; the dashboard owns every value thereafter; no UI badges. The original ExistingRow_NotOverwrittenByEnv test asserting exactly this is restored, alongside a managed-mode counterpart — both contracts are pinned under their respective modes.

  • true (the pdn supervised-app case): at every startup, any set DAPPS_<KEY> whose value differs from the stored row updates the row and logs:

    SystemOption {key} applied from environment (DAPPS_{KEY}) — this value is deployment-managed; dashboard edits will be overridden while the variable remains set

    UI: /Settings and /Setup badge any field whose env var is currently set with a "managed by environment" marker (tooltip names the variable) and render the control read-only/disabled. Saving unrelated fields still round-trips the managed values intact.

The repo-root pdn-app.yaml opts supervised deployments in (and its comment block explains both modes):

service:
  command: ./dapps
  environment:
    DAPPS_ENV_MANAGED: "true"             # opt in: env vars below are re-applied at every start
    DAPPS_NODE_BEARER: rhpv2
    #

docs/configure.md gets the same note; its documented standalone first-start-only contract is unchanged.

The seed call in Program.cs now gets a bootstrap console logger — previously it passed no logger, so none of the seeding decisions were visible in the journal.

2. Callsign derived from the host node — now probing for a free SSID

"as a rule I would generally expect DAPPS to reside at an SSID of the node callsign"
"Any way for the setup flow to check for free/taken SSIDs, and pick one?"

When the stored callsign is absent or the N0CALL placeholder and the pdn host injects PDN_NODE_CALLSIGN, DAPPS derives its identity as <base-of-node-callsign>-<Ssid> (Ssid is a new seeded option, default 7, env-overridable as DAPPS_SSID). This derivation is deliberately not gated on DAPPS_ENV_MANAGED — standalone installs have no PDN_NODE_CALLSIGN, so it cannot affect them.

And instead of blindly trusting -7, the derivation is now confirmed over RHPv2 itself. pdn answers a listen on an already-claimed callsign — including the node's own — with errCode 9 "Duplicate socket", deterministically (its docs/rhp2-server.md deviation D5). So:

  • DbStartup records each derivation in a DerivedCallsignPending row. While that marker still matches the callsign in use (and no explicit DAPPS_CALLSIGN is set), Rhpv2InboundService treats a 9 on bind+listen as "taken" and walks the candidates: start+1 … 15, then 1 … start−1, skipping 0 and the SSID the node itself uses (parsed off PDN_NODE_CALLSIGN). First successful listen wins.
  • The winner is persisted as the stored callsign (one log line: derived callsign M9YYY-8 — -7 was taken on the node) and the marker is cleared, so the identity is stable on every later start — a persisted, confirmed callsign never walks again.
  • An explicitly configured callsign (env, dashboard, or already-confirmed) never walks: a 9 logs the refusal and keeps the existing retry/reconnect cadence.
  • All candidates taken → log loudly and revert to setup-required mode (no spinning against the node); the next restart re-derives and probes again.
  • Against a server that answers duplicate listens Ok (live XRouter does), the walk simply never triggers — fine, since derivation only runs under pdn supervision.

Precedence (highest first): explicit DAPPS_CALLSIGN env → real stored callsign (dashboard-configured / probe-confirmed) → derivation+probe → N0CALL placeholder (standalone, setup-required mode as today). /Setup still prefills its callsign field with the derived value in the rare case the bearer step renders while PDN_NODE_CALLSIGN is present.

⚠️ Tom's call — proposed default DAPPS_SSID=7: there is no packet-wide DAPPS SSID convention yet, so this PR proposes one. -7 matches the M0LTE-7 identity the pdn acceptance testing used. It's a one-line constant (DbStartup.DefaultSsid) if you want a different number — and with the probe-walk, a collision at -7 now resolves itself anyway.

Release asset

The release job now attaches the repo-root pdn-app.yaml (#148) next to the binaries, with its informational version: stamped to the release tag, so the pdn build fetches manifest + binary from the same release instead of carrying an interim copy.

Tests

dapps.core.tests: 674 passed, 0 failed (branch baseline: 660/0; pre-PR baseline: 651/0). The PR's flipped ExistingRow_… test is un-flipped — the restored standalone contract plus a DAPPS_ENV_MANAGED=true counterpart, =false-same-as-unset, and badge gating. New DbStartupTests cover the pending-marker lifecycle (set on derivation, survives unconfirmed restarts, cleared by dashboard/env-pinned identities, confirm/abandon round-trips). New Rhpv2SsidProbeWalkTests drive the walk against MockRhpServer playing pdn's D5 role, through the real SystemOptionsStore persist→reload path: walk-on-9 picks the next free SSID + persists + is stable across restart; an explicit callsign never walks; non-9 errors never walk; all-taken walks in the exact documented order then reverts to setup-required; plus the candidate-order unit test.

Also verified live earlier on this branch: fresh start under pdn-style env derives and seeds correctly; a restart re-applies an env var over a dashboard edit (with the log line above) while leaving a dashboard-pinned real callsign alone — that behaviour now sits behind DAPPS_ENV_MANAGED=true.

🤖 Generated with Claude Code

M0LTE and others added 2 commits June 11, 2026 10:20
…ost node

Two startup-config behaviours for the pdn supervised-app deployment, both inert for standalone installs:

1. Set DAPPS_* env vars are now re-applied at every start, not just first start. A stored row that differs from its set env var is updated and logged as deployment-managed. Unset env vars never touch stored config, so the standalone /Setup // /Config flow is unchanged, and seeding once via env then unsetting keeps dashboard control as before. /Settings and /Setup badge env-managed fields ("managed by environment") and render them read-only.

2. When the stored callsign is absent or the N0CALL placeholder and the pdn host injects PDN_NODE_CALLSIGN, the callsign is derived as <node-base-call>-<Ssid>, where Ssid is a new seeded option (DAPPS_SSID) defaulting to "7" - proposing the convention that DAPPS resides at SSID -7 of the node callsign. Any SSID on PDN_NODE_CALLSIGN is stripped first. An explicit DAPPS_CALLSIGN or a real stored callsign always wins. /Setup prefills the derived value when applicable.

Also: the release job now stamps pdn-app.yaml's version to the release tag and attaches it as a release asset so pdn's build can fetch manifest + binary from one release; pdn-app.yaml's seeding notes updated to match (no DAPPS_CALLSIGN pre-set needed under pdn any more); the seed call in Program.cs now gets a bootstrap console logger so these decisions are visible in the journal.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…for a free SSID

Amendment 1 (review fix - the re-apply semantics were a breaking change for standalone installs): scripts/dapps.service keeps EnvironmentFile=/etc/dapps.env applied forever, so re-applying set DAPPS_* values at every start would silently revert dashboard edits for every documented standalone install. The deployment-managed behaviour is now gated behind DAPPS_ENV_MANAGED=true, a mode switch read each start and never stored. Unset/false keeps today's byte-for-byte semantics: env vars seed missing rows on first start only, no UI badges. True gives the deployment-managed semantics exactly as built: re-apply at every start + journal line + read-only "managed by environment" badges. pdn-app.yaml opts supervised deployments in and documents both modes; docs/configure.md gets the same note. The original ExistingRow_NotOverwrittenByEnv test is restored verbatim and both contracts are pinned under their respective modes. The callsign derivation stays ungated - it only fires when the callsign is placeholder/absent AND PDN_NODE_CALLSIGN is present, which standalone installs never have.

Amendment 2: instead of blindly trusting <nodecall>-<DAPPS_SSID>, a derived callsign now checks for free/taken SSIDs over RHPv2 itself. pdn refuses a listen on an already-claimed callsign (including the node's own) with errCode 9 "Duplicate socket", deterministically (packet.net docs/rhp2-server.md deviation D5). DbStartup records each derivation in a DerivedCallsignPending row; while that marker still matches the callsign in use (and no explicit DAPPS_CALLSIGN is set), Rhpv2InboundService treats a 9 as "taken" and walks the candidate SSIDs - start+1..15, then 1..start-1, skipping 0 and the SSID the node itself uses (parsed off PDN_NODE_CALLSIGN). The first successful listen wins and is persisted as the stored callsign (one log line: derived callsign M9YYY-8 - -7 was taken on the node), so the identity is stable on every later start; a persisted, confirmed callsign never walks again. An explicitly configured callsign NEVER walks - a 9 logs the refusal and keeps the existing retry/reconnect behaviour. Non-9 errors never walk. All candidates taken logs loudly and reverts to setup-required mode instead of spinning; the next restart re-derives and probes again. Against a server that answers duplicate listens Ok (live XRouter does), the walk simply never triggers - fine, since derivation only runs under pdn supervision.

dapps.core.tests: 674 passed, 0 failed (branch baseline 660; +14: both env modes incl. the restored standalone contract, badge gating, the pending-marker lifecycle, and the probe-walk against MockRhpServer playing pdn's D5 role - walk+persist+stable-across-restart, explicit-never-walks, non-9-never-walks, all-taken-reverts, and the candidate-order unit test).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@M0LTE M0LTE changed the title Config: environment values are deployment-managed; derive callsign from the host node Config: deployment-managed env values (opt-in via DAPPS_ENV_MANAGED); derive the DAPPS callsign from the host node, probing for a free SSID Jun 11, 2026
DAPPS_ENV_MANAGED opt-in deployment-managed config + node-derived callsign with SSID probe-walk + pdn-app.yaml release asset.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@M0LTE
M0LTE merged commit 0f77d1d into master Jun 11, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant