Skip to content

fix: enforce the release policy end to end and serialise overseer decisions - #39

Merged
vbabenko97 merged 1 commit into
mainfrom
babenko/disclosure-governance
Sep 25, 2026
Merged

vbabenko97 merged 1 commit into
mainfrom
babenko/disclosure-governance

Conversation

@vbabenko97

Copy link
Copy Markdown
Collaborator

Summary

Closes gaps where aggregates could leave the server without passing the full release policy, and makes the overseer release lifecycle consistent: one lock, atomic writes, and run status that follows overseer decisions.

Changes

Disclosure control

  • projects.yaml sets a per-project min_cell_size floor of 5. Requests may raise k but never lower it; the adapters and server/disclosure_check.py both apply the floor.
  • A variable observed in fewer than k rows releases nothing.
  • Regression inputs with missing values are rejected on every backend. The SQL adapter counts complete rows in the same aggregate query.
  • Every Newton-Raphson round goes through one helper, safe_output.release_irls_step, which checks shape, finiteness and n >= max(k, p + 2). Each round is audited whether accepted or rejected.
  • The dominance rule also covers per-variable counts, Gram sample counts and every released round.
  • Every released round needs at least two sites.
  • fed_stats results need observed values for every requested variable.
  • Released results name each suppressed location but not the count behind it. result.json keeps the full detail for overseers.

Release lifecycle

  • overseer_queue.RELEASE_LOCK is re-entrant. It covers check → record and every approve/reject.
  • Queue and released.json writes are atomic.
  • A rerun of a spec replaces the earlier run's release or queue entry. released.json exists only while the latest run is OK or approved.
  • Run status follows overseer decisions, including a decision that lands between record and the run update. The UI reloads the open run after a decision.

Operations and CI

  • scripts/limit_server_jobs.py makes a provisioned server kit run one job at a time (max_jobs: 1). It writes a full copy of resources.json.default, which NVFlare uses in place of the default. provision.sh and local_federation.sh call it.
  • CI adds a merge-conflict-marker check and a frontend job (Node 24: npm ci, npm test, npm run build).
  • Frontend dependencies are pinned to exact versions.

Behaviour changes to note

  • Regression specs with missing values in model columns are now rejected. Previously the SQL backend summed each Gram term over a different set of rows while counting every row in n, which gave a wrong fit with no warning. The pandas-based backends produced NaN statistics.
  • Logistic fits report sites_reported, n and n_per_site only from sites that contributed an accepted round, on both the API and FLARE paths. A site that joined and then failed is now reported as missing.
  • FLARE training stops at any round with fewer than two sites.
  • FLARE FedAvg no longer records a result when no training round completes. It used to release its zero start vector as the fit.
  • RunStatus gains status rejected and decisions APPROVED / REJECTED. docs/schemas/ has been regenerated.
  • The frontend proxy's default backend changed from :8080 to :8500, the documented API port. frontend/.env.example matches.

Verification

Check Result
python -m pytest -q -m "not slow" 149 passed, including the schema-freshness test
python -m pytest -q -m slow (FLARE simulator: allele_freq, straggler, FedAvg linreg, Newton-Raphson logreg) 4 passed
Frontend npm test, npm run build 6 passed, build OK. Run before the final Python-only fix round; no frontend files changed after it. These tests cover rendering only.
Provisioned server kit max_jobs: 1, rest of the config preserved, NVFlare selects the override
git diff --check, conflict-marker grep, CI YAML parse, shell syntax clean

An independent review of the final tree found nothing new to fix and reran both Python suites. Not run on this branch: the Docker stack, or a live local_federation.sh job against a kit limited to max_jobs: 1.

Accepted limitations

  • No authentication or authorization. The governance endpoints (/overseer, /overseer/{hash}/approve|reject, /audit) are open, and /overseer and /audit show exact pre-suppression counts. Authentication and authorization for these endpoints are mandatory before any shared deployment.
  • One process per SERVER_OUT. The lock is in-process only. The API, a FLARE server and the overseer CLI writing the same directory at the same time are not coordinated.
  • Approval is keyed by spec hash. If the same spec reruns between an overseer viewing a flagged result and approving it, the approval applies to the newer result.
  • Run state lives in memory; durable run storage is deferred to deployment.
  • min / max are released as exact values.
  • FedAvg on FLARE uses every site that joined in its standardisation n, including sites that later miss rounds.
  • Kits provisioned before this change keep NVFlare's default (max_jobs: 4) until they are re-provisioned or scripts/limit_server_jobs.py is run on them. Deployed kits have not been inspected.
  • The frontend approve/reject → result-refresh flow has no automated test. That is the next priority.

…isions

Disclosure control
- projects.yaml sets a per-project min_cell_size floor (5). A request may
  ask for a larger k, never a smaller one; the adapters and the server
  disclosure check both apply the floor.
- A variable observed in fewer than k rows releases nothing.
- Regression inputs with missing values are rejected on every backend
  instead of being fitted on a silent subset. The SQL adapter counts
  complete rows in the same aggregate query.
- Every Newton-Raphson round goes through one release helper (shape,
  finiteness, n >= max(k, p + 2)) and is audited whether it is accepted
  or rejected, on the direct API path and in the FLARE executor.
- The dominance rule also covers per-variable counts, Gram sample counts
  and every released round, and every released round needs two sites.
- Logistic fits take sites_reported, n and n_per_site from the sites that
  contributed an accepted round, on both the API and FLARE paths.
- fed_stats results need observed values for every requested variable.
- Released results name each suppressed location without the count that
  caused the suppression; result.json keeps the detail for overseers.

Release lifecycle
- One re-entrant lock in server/overseer_queue.py covers disclosure check
  -> record and every approve/reject decision. Queue and released.json
  writes are atomic.
- A new run of a spec replaces the earlier run's release or queue entry,
  and a rejected result leaves nothing released.
- Run status reports overseer decisions (APPROVED / REJECTED, status
  "rejected"), including a decision that lands between record and the run
  update. The UI reloads the open run after a decision.
- The FLARE FedAvg controller no longer records its zero start vector as
  a fit when no training round completes.
- Two processes writing one SERVER_OUT stay unsupported; the docs say so.

Operations and CI
- scripts/limit_server_jobs.py makes a provisioned server kit run one job
  at a time (max_jobs 1, full copy of resources.json.default);
  provision.sh and local_federation.sh call it.
- CI adds a merge-conflict-marker check and a frontend job (Node 24:
  npm ci, npm test, npm run build). Frontend dependencies are pinned and
  the UI proxy defaults to the API on :8500.

Tests: 149 fast and 4 FLARE simulator tests pass.
@vbabenko97
vbabenko97 merged commit 1cc7040 into main Sep 25, 2026
4 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