Crosswake.Companion behaviour (crosswake v0.1.2)

View Source

Behaviour for first-party Phoenix-native companion integrations.

A companion is a bounded integration seam between Crosswake's route-policy system and an external Elixir library (e.g. rulestead for feature flags, rindle for media, sigra for auth). Companions live in-tree under lib/crosswake/companions/<name>/ for the v3.5 milestone and may be extracted to separate packages in a future milestone once the seam stabilizes.

Implementing a companion

Declare @behaviour Crosswake.Companion in your module and implement all six required callbacks. No use Crosswake.Companion macro exists — the behaviour is deliberately thin (D-12 conceptual lineage from Crosswake.Commerce).

Register companions at compile time in your host application config:

config :crosswake, :companions, [MyApp.Companions.Rulestead]

Telemetry events

Crosswake emits the following static telemetry event-name contracts for companion spans. All events are differentiated by %{companion_id: atom(), route_id: binary() | nil} metadata per Keathley conventions.

  • [:crosswake, :companion, :validate_dependency, :start | :stop | :exception] — emitted in Phase 38 (Plan 02) when the doctor runs validate_dependency/0 for each registered companion. The :stop metadata includes result: :ok | {:error, [module()]}.

  • [:crosswake, :companion, :route_gate, :start | :stop | :exception] — specified now, emitted in Phase 40 when RouteGate calls route_gated?/2.

  • [:crosswake, :companion, :kill_switch, :start | :stop | :exception] — specified now, emitted in Phase 40 when RouteGate calls kill_switch_active?/1 (short-circuits ahead of route_gated?/2).

Summary

Callbacks

Returns the unique atom identifier for this companion.

Returns whether this companion is enabled for the current host configuration.

Returns whether the companion's kill switch is currently active.

Reports the current runtime state of this companion as a typed struct.

Evaluates whether a specific route is gated by this companion's policy.

Validates that all optional dependencies required by this companion are loaded.

Callbacks

companion_id()

@callback companion_id() :: atom()

Returns the unique atom identifier for this companion.

Used as the companion_id key in telemetry metadata and in Crosswake.Companion.State reports.

enabled?(config)

@callback enabled?(config :: map()) :: boolean()

Returns whether this companion is enabled for the current host configuration.

Receives the host-owned config map (arbitrarily shaped, narrowed internally by the companion implementation — FunWithFlags-style). This is a host-level toggle, NOT a per-route gate (D-03); per-route policy is route_gated?/2.

The host passes a config map supplied via Application.get_env/3 or equivalent. The companion is responsible for extracting and interpreting its own keys from the map.

kill_switch_active?(context)

@callback kill_switch_active?(context :: Crosswake.Compatibility.Target.t()) :: boolean()

Returns whether the companion's kill switch is currently active.

Kill switches are route-independent (D-07) — this callback receives only Target.t() context, not a route, so kill-switch logic can short-circuit ahead of route_gated?/2 without per-route evaluation overhead.

Wired into RouteGate in Phase 40. Returns true when the kill switch is active (route access should be denied for this companion); false otherwise.

report_state()

@callback report_state() :: Crosswake.Companion.State.t()

Reports the current runtime state of this companion as a typed struct.

Returns a Crosswake.Companion.State.t() snapshot at the moment of the call. checked_at should be System.monotonic_time(:millisecond).

The gate_status and kill_switch_status fields are defined in the type space now but are only meaningfully populated starting in Phase 40/41 when the gating and kill-switch machinery is wired. Implementations should return :unconfigured for these fields until Phase 40 wiring is complete.

route_gated?(route, context)

@callback route_gated?(
  route :: Crosswake.Manifest.Types.RouteEntry.t(),
  context :: Crosswake.Compatibility.Target.t()
) :: {:deny, Crosswake.Compatibility.Finding.t()} | :pass

Evaluates whether a specific route is gated by this companion's policy.

Returns {:deny, Finding.t()} with evidence the policy compiler consumes, or :pass (not nil) to keep the return type closed (D-06). :pass is the explicit non-denial value — there is no bare term() escape hatch.

A companion can only FURTHER-RESTRICT access; it can never open a route that has already been denied by the core policy. The RouteGate consumes this return in Phase 40 (defined-not-wired here).

kill_switch_active?/1 short-circuits ahead of this callback — if the kill switch is active, route_gated?/2 is not called.

validate_dependency()

@callback validate_dependency() :: :ok | {:error, [module()]}

Validates that all optional dependencies required by this companion are loaded.

Returns :ok if all required modules are present, or {:error, [module()]} with the list of module(s) that failed Code.ensure_loaded?/1 (Swoosh-style missing-module list, D-08).

The doctor wraps this call in a [:crosswake, :companion, :validate_dependency] telemetry span and emits a :companion.dependency_missing finding when this returns an error AND the companion reports enabled?: true.