fleetd #111: probe reads the live memberCredentials policy, no hardcoded name list #256

Closed
agent wants to merge 0 commits from worker/fleetd-111-7e8673-9 into main
Member

fleetd #111 (CB-608): probe reads the live memberCredentials policy, no hardcoded name list

The defect

scripts/probe-member-credentials.sh carried its own hand-maintained NAMES array (31 names,
recorded 2026-08-16). The live memberCredentials.known: policy in fleetd.yaml can grow past
that list, and the probe never noticed — it kept checking the same 31 names, exited 0, and
printed a table that looked complete. Same "hand-maintained second copy drifts" shape as #114.

What changed

  • New dev.ltms.fleet.member.MemberCredentialPolicyView — the one place that turns a
    FleetConfig.MemberCredentials policy into names + counts. Names and counts only, never a
    value (the daemon never holds a credential's value in the first place, only the name it is
    configured under — this is called out in the class javadoc).
  • Fleetd.reportMemberCredentialsGap (the startup log line) now builds this view instead of
    hand-counting creds.known().size() etc. inline — one code path, not two.
  • FleetApp gains GET /member-credentials (names/counts/policy mode as JSON) plus a
    Supplier<MemberCredentialPolicyView> constructor parameter. Every legacy constructor defaults
    it to MemberCredentialPolicyView::absent, so existing test call sites are unaffected.
  • Fleetd.java footprint: one import, one added constructor argument at the existing
    new FleetApp(...) call site, and the reportMemberCredentialsGap body rewritten to reuse the
    view. Nothing else in the file touched — no restructuring, no reformatting, to stay mergeable
    alongside the two other in-flight edits to Fleetd.java/FleetConfig.java. FleetConfig.java
    itself is untouched.
  • scripts/probe-member-credentials.sh now fetches its name list from
    GET /member-credentials (FLEETD_HOST, default http://127.0.0.1:8765) instead of carrying
    one. No local fallback list, ever:
    • an unreachable daemon → refuse, non-zero exit
    • an absent/empty policy (knownCount == 0 or an empty parsed known[]) → refuse, non-zero exit
    • a knownCount vs. parsed known[] length mismatch → refuse, non-zero exit
    • it prints policy contains N name(s); this run checked N — they match so the two numbers
      are visibly equal.
    • JSON is parsed with jq if present, else python3; neither present → refuse loudly (no
      grep/sed JSON guessing).
    • Found and fixed a real bug while writing this: jq's // operator treats false/0 as
      "missing", so a naive .present // empty silently turned a real "present": false into
      "unknown". Fixed by extracting fields directly (| tostring) instead of via // default.

Tests

  • MemberCredentialPolicyViewTest (5 tests, new): counts match a real policy, a null policy and
    an empty-known policy are both reported as absent (never as "nothing blocked"), a present
    policy with blockedCount == 0 is still distinguishable from absent, and names pass through
    unchanged (no value ever substituted).
  • Mutation test: changed MemberCredentialPolicyView.of(...) to always return ABSENT. Result:
    4 tests went red — 3 in the new test class plus MemberCredentialsGapReportTest .aPopulatedKnownListLogsInfoNotWarn (confirming the startup log line genuinely shares this
    code path). 0 compile errors — a real kill, not a build break. Reverted;
    diff -q confirms the file is back to the original, and a re-run of both test classes is green
    again.
  • Manually exercised the probe script's new logic end-to-end against local mock HTTP servers on
    a spare port (not the live daemon, which does not have the new route until this deploys):
    success path via jq, the jq false/// empty bug (found and fixed), unreachable-daemon
    refusal, empty-policy refusal, and knownCount/known[] length-mismatch refusal. The python3
    fallback parser was checked directly against sample JSON (not through the live script, since
    jq is present on this host and couldn't easily be hidden from PATH).

Build

cd fleetd && mvn clean install (no pipe), full run: 1239 tests, 0 Failures, 0 Errors — BUILD
SUCCESS.

What I could not do / did not claim

  • Cannot verify against the live policy — fleetd/fleetd.yaml is gitignored and not in my
    worktree, and I don't own the running daemon. Ticket acceptance criterion 5 (re-run the probe
    against the live daemon and confirm 34 checked / 29 blocked / 5 allowed) is explicitly the
    lead's to run after redeploying, not mine.
  • Confirmed git log --oneline --grep='#111' origin/main is empty before starting — this was
    not already fixed.

Same-shape note (reported, not fixed — out of scope)

  • scripts/rename-checkout.sh and scripts/redeploy-fleetd.sh both hardcode
    HEALTH='http://127.0.0.1:8765/healthz' independently rather than sharing one constant — low
    risk (both point at the same fixed default and both allow override via the same convention),
    but it's the same "two files, one fact" shape as this ticket if that port/path ever changes.

Refs #111.

## fleetd #111 (CB-608): probe reads the live memberCredentials policy, no hardcoded name list ### The defect `scripts/probe-member-credentials.sh` carried its own hand-maintained `NAMES` array (31 names, recorded 2026-08-16). The live `memberCredentials.known:` policy in `fleetd.yaml` can grow past that list, and the probe never noticed — it kept checking the same 31 names, exited 0, and printed a table that looked complete. Same "hand-maintained second copy drifts" shape as #114. ### What changed - **New `dev.ltms.fleet.member.MemberCredentialPolicyView`** — the one place that turns a `FleetConfig.MemberCredentials` policy into names + counts. Names and counts only, never a value (the daemon never holds a credential's value in the first place, only the name it is configured under — this is called out in the class javadoc). - **`Fleetd.reportMemberCredentialsGap`** (the startup log line) now builds this view instead of hand-counting `creds.known().size()` etc. inline — one code path, not two. - **`FleetApp`** gains `GET /member-credentials` (names/counts/policy mode as JSON) plus a `Supplier<MemberCredentialPolicyView>` constructor parameter. Every legacy constructor defaults it to `MemberCredentialPolicyView::absent`, so existing test call sites are unaffected. - **`Fleetd.java`** footprint: one import, one added constructor argument at the existing `new FleetApp(...)` call site, and the `reportMemberCredentialsGap` body rewritten to reuse the view. Nothing else in the file touched — no restructuring, no reformatting, to stay mergeable alongside the two other in-flight edits to `Fleetd.java`/`FleetConfig.java`. `FleetConfig.java` itself is untouched. - **`scripts/probe-member-credentials.sh`** now fetches its name list from `GET /member-credentials` (`FLEETD_HOST`, default `http://127.0.0.1:8765`) instead of carrying one. No local fallback list, ever: - an unreachable daemon → refuse, non-zero exit - an absent/empty policy (`knownCount == 0` or an empty parsed `known[]`) → refuse, non-zero exit - a `knownCount` vs. parsed `known[]` length mismatch → refuse, non-zero exit - it prints `policy contains N name(s); this run checked N — they match` so the two numbers are visibly equal. - JSON is parsed with `jq` if present, else `python3`; neither present → refuse loudly (no grep/sed JSON guessing). - Found and fixed a real bug while writing this: `jq`'s `//` operator treats `false`/`0` as "missing", so a naive `.present // empty` silently turned a real `"present": false` into "unknown". Fixed by extracting fields directly (`| tostring`) instead of via `// default`. ### Tests - `MemberCredentialPolicyViewTest` (5 tests, new): counts match a real policy, a null policy and an empty-`known` policy are both reported as absent (never as "nothing blocked"), a present policy with `blockedCount == 0` is still distinguishable from absent, and names pass through unchanged (no value ever substituted). - **Mutation test**: changed `MemberCredentialPolicyView.of(...)` to always return `ABSENT`. Result: 4 tests went red — 3 in the new test class plus `MemberCredentialsGapReportTest .aPopulatedKnownListLogsInfoNotWarn` (confirming the startup log line genuinely shares this code path). 0 compile errors — a real kill, not a build break. Reverted; `diff -q` confirms the file is back to the original, and a re-run of both test classes is green again. - Manually exercised the probe script's new logic end-to-end against local mock HTTP servers on a spare port (not the live daemon, which does not have the new route until this deploys): success path via `jq`, the `jq` `false`/`// empty` bug (found and fixed), unreachable-daemon refusal, empty-policy refusal, and knownCount/`known[]` length-mismatch refusal. The python3 fallback parser was checked directly against sample JSON (not through the live script, since `jq` is present on this host and couldn't easily be hidden from `PATH`). ### Build `cd fleetd && mvn clean install` (no pipe), full run: **1239 tests, 0 Failures, 0 Errors — BUILD SUCCESS.** ### What I could not do / did not claim - Cannot verify against the *live* policy — `fleetd/fleetd.yaml` is gitignored and not in my worktree, and I don't own the running daemon. Ticket acceptance criterion 5 (re-run the probe against the live daemon and confirm 34 checked / 29 blocked / 5 allowed) is explicitly the lead's to run after redeploying, not mine. - Confirmed `git log --oneline --grep='#111' origin/main` is empty before starting — this was not already fixed. ### Same-shape note (reported, not fixed — out of scope) - `scripts/rename-checkout.sh` and `scripts/redeploy-fleetd.sh` both hardcode `HEALTH='http://127.0.0.1:8765/healthz'` independently rather than sharing one constant — low risk (both point at the same fixed default and both allow override via the same convention), but it's the same "two files, one fact" shape as this ticket if that port/path ever changes. Refs #111.
agent added 1 commit 2026-09-03 08:22:17 +02:00
fleetd #111: probe reads the live memberCredentials policy, no hardcoded name list
CI / contract (pull_request) Successful in 1m24s
CI / build (pull_request) Successful in 2m22s
51f7b0a3ca
scripts/probe-member-credentials.sh carried its own hand-maintained NAMES array
(31 names, recorded 2026-08-16), so a name added later to fleetd.yaml's
memberCredentials.known was never checked and the probe still exited 0 with a
clean-looking table. Same drift shape as #114's tool catalogue.

- New dev.ltms.fleet.member.MemberCredentialPolicyView: the single place that
  turns a MemberCredentials policy into names + counts (never a value). Reused
  by Fleetd.reportMemberCredentialsGap (startup log line) and by the new
  GET /member-credentials REST endpoint (FleetApp), so the two can no longer
  drift apart the way the probe and the policy did.
- FleetApp gains one route + handler + a Supplier<MemberCredentialPolicyView>
  constructor param (legacy constructors default to ::absent, so existing call
  sites are unaffected).
- probe-member-credentials.sh now fetches its name list from
  GET /member-credentials instead of carrying one. No local fallback: an
  unreachable daemon, an empty/absent policy, or a knownCount/known[] length
  mismatch all refuse with a non-zero exit rather than silently checking zero
  names. Prints "policy contains N; this run checked N" so the two numbers are
  visibly equal.
Owner

Merged into main as 6b5f3f4. Closing manually — the merge went in from the worktree, not through the Gitea merge button.

Verified live after redeploying the daemon onto this jar, in a real member pane:

policy       : mode=allow-list known=34 allowed=7 blocked=29
policy contains 34 name(s); this run checked 34 — they match.
5 of 34 names are set in this shell.

That is acceptance criterion 5 met exactly: 34 checked, 29 blocked, 5 allowed-and-present. The old probe checked 31 hardcoded names and reported 26.

Two things you did that were worth more than the diff: keeping the Fleetd.java footprint to one route registration and one argument, so this merged cleanly alongside two other workers editing that same file — and catching the jq // bug in your own draft, where false and 0 read as missing. That one would have turned a real "present": false into "unknown" and nobody would have noticed.

Full write-up on #111. Your HEALTH='http://127.0.0.1:8765/healthz' observation across two scripts is the same shape and correctly left alone.

Merged into `main` as `6b5f3f4`. Closing manually — the merge went in from the worktree, not through the Gitea merge button. Verified live after redeploying the daemon onto this jar, in a real member pane: ``` policy : mode=allow-list known=34 allowed=7 blocked=29 policy contains 34 name(s); this run checked 34 — they match. 5 of 34 names are set in this shell. ``` That is acceptance criterion 5 met exactly: 34 checked, 29 blocked, 5 allowed-and-present. The old probe checked 31 hardcoded names and reported 26. Two things you did that were worth more than the diff: keeping the `Fleetd.java` footprint to one route registration and one argument, so this merged cleanly alongside two other workers editing that same file — and catching the jq `//` bug in your own draft, where `false` and `0` read as missing. That one would have turned a real `"present": false` into "unknown" and nobody would have noticed. Full write-up on #111. Your `HEALTH='http://127.0.0.1:8765/healthz'` observation across two scripts is the same shape and correctly left alone.
ltms closed this pull request 2026-09-03 08:40:35 +02:00
Some checks are pending
CI / contract (pull_request) Successful in 1m24s
CI / build (pull_request) Successful in 2m22s

Pull request closed

Sign in to join this conversation.