Skip to content

feat: client feature flags - #2637

Open
denys-gif wants to merge 6 commits into
mainfrom
feat/client-feature-flags
Open

denys-gif wants to merge 6 commits into
mainfrom
feat/client-feature-flags

Conversation

@denys-gif

@denys-gif denys-gif commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Adds client-side feature flags pushed by the backend through the delivery engine. Flags are applied only at startup. When a new set changes what the running client would enable, the client drains its work and restarts itself through a detached restart-service process.

To get there, the PR adds a generic Core-NATS command listener (listener/command/) with delivery-engine results (ACKED / DONE / FAILED), a restart gate that waits for in-flight work, and a NATS integration test harness that uses a real nats-server.

No production code reads the flags yet. The three flags are placeholders for moving tool installation, client uninstall and client update onto the delivery engine.

Wire contract

Inbound: Core NATS machine.<machineId>.feature-flags

{
  "delivery": { "type": "CLIENT_FEATURE_FLAGS", "targetId": "...", "dispatchId": "...", "sequence": 11 },
  "flags": {
    "delivery-tool-installation": { "enabled": true,  "minVersion": "1.6.0" },
    "delivery-client-uninstall":  { "enabled": false, "minVersion": "1.6.0" },
    "delivery-client-update":     { "enabled": false, "minVersion": "1.6.0" }
  }
}

Outbound: machine.<machineId>.delivery.result

{ "delivery": { "...": "same ref" }, "result": "ACKED | DONE | FAILED", "error": "..." }
  • ACKED is published directly (publish + flush).
  • DONE and FAILED go through the durable redb outbox and are published with a JetStream ack.
  • For a flag change that needs a restart, DONE is sent by the new process after the restart.

Behaviour

Effective value per flag:

  • On the dev build (0.0.0-dev), take enabled.
  • Otherwise, take enabled if the client version is ≥ minVersion (semver, pre-release aware).
  • Otherwise, fall back to the feature default. All defaults are false.

Validation:

  • An entry without a boolean enabled and a semver minVersion is dropped with a warning.
  • Unknown flag names are kept on disk but ignored.

Ordering: a per-machine sequence gate. A stale or duplicate set is answered DONE without being applied. A new registration (different machine id) accepts any sequence.

Persistence: secured/feature_flags.json, written atomically, holds machine_id, sequence, flags, and an optional awaiting_restart delivery ref. A missing or unreadable file means all features are off.

Restart only when needed: the client restarts only if the resolved set differs from the running one. Changes that don't apply to this version, a moved minVersion that still applies, or unknown flags answer DONE immediately.

Drain (RestartGate):

  1. Closes admission for Core commands.
  2. Waits for admitted commands, busy tool operations and running scripts, up to 600 s. After that it restarts anyway.
  3. A client update that is actually running is waited past the cap, bounded by its pending TTL, so the restart never stops the service mid binary swap.
  4. A client update that is only deferred because a tool is busy does not hold the restart.

Handoff:

  • The client spawns the hidden openframe-client restart-service detached (macOS setsid, Windows DETACHED_PROCESS). It stops and starts the service.
  • If this process is still alive once the child finishes, or after 900 s, it exits with code 75 and the service manager restarts it.
  • Linux has no detached spawn, so it goes straight to exit(75).

Startup order: a pending self-update is handled before the flags listener starts, so a flags restart can't cut post-update verification short.

Notable changes outside the new module

  • platform/detached_process.rs: spawn_detached() is extracted from self-uninstall and shared with restart-service. Uninstall behaviour is unchanged.
  • RestartGate reads narrow read-only views instead of whole services: ToolOps::busy_tools(), ExecutionService::running_scripts() (new running-script counter) and ToolRunManager::client_update_pending().
  • ClientUpdatePendingFlag now uses a std Mutex, so its methods are sync. Call sites in openframe_client_update_service.rs and client_update_gate.rs only drop .await.
  • is_ack_subject is renamed to requires_stream_ack and now also covers delivery.result.
  • NatsConnectionManager::use_client_for_tests (#[cfg(test)]) for the integration harness.

@denys-gif denys-gif self-assigned this Oct 12, 2026
@github-actions

Copy link
Copy Markdown
Contributor

🦩 Flamingo Code Review

Not reviewed yet. Reviews start when you ask, so the reviewer reads this pull request when it is ready, not when it is opened.

When it is ready:

  • Review this pull request

Or comment @flamingo-review. Add the flamingo-review-always label to review every push.

Updated 2026-10-12 18:05 UTC · workflow run

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.

2 participants