Packages

CrucibleFramework: A thin orchestration layer for experiment pipelines. Provides pipeline execution, stage behaviour, and optional persistence.

Current section

Files

Jump to
crucible_framework lib crucible tinkex api router.ex
Raw

lib/crucible/tinkex/api/router.ex

defmodule Crucible.Tinkex.API.Router do
@moduledoc """
REST/WebSocket scaffolding for the Crucible Tinkex overlay.
This module defines controller-like functions that mirror the CNS blueprint
endpoints:
- POST /v1/jobs
- GET /v1/jobs/:id
- GET /v1/jobs/:id/stream (SSE/WebSocket)
- POST /v1/jobs/:id/cancel
The functions are framework-agnostic so they can be wired into Phoenix or
any Plug-compatible router without pulling UI logic into the core library.
"""
alias Crucible.Tinkex.API.Auth
alias Crucible.Tinkex.Job
alias Crucible.Tinkex.JobQueue
alias Crucible.Tinkex.JobStore
alias Crucible.Tinkex.TelemetryBroker
@type request :: %{params: map(), headers: [{String.t(), String.t()}]}
@doc """
Handles job submission (POST /v1/jobs).
"""
@spec submit(request()) ::
{:ok, map()} | {:error, :unauthorized} | {:error, :invalid_request}
def submit(%{params: params, headers: headers}) do
with {:ok, actor} <- Auth.verify(headers),
{:ok, job} <- Job.new(params),
{:ok, _} <- JobQueue.enqueue(job) do
token = TelemetryBroker.issue_stream_token(job.id, actor: actor)
{:ok,
%{
job_id: job.id,
status: :queued,
stream_token: token,
artifacts_path: job.artifacts_path
}}
else
{:error, :unauthorized} = error -> error
_ -> {:error, :invalid_request}
end
end
@doc """
Fetches job status and latest metadata (GET /v1/jobs/:id).
"""
@spec fetch(request(), String.t()) :: {:ok, map()} | {:error, :unauthorized | :not_found}
def fetch(%{headers: headers}, job_id) do
with {:ok, _} <- Auth.verify(headers),
{:ok, job} <- JobStore.get(job_id) do
{:ok,
%{
job_id: job.id,
status: job.status,
spec: job.spec,
artifacts_path: job.artifacts_path,
inserted_at: job.inserted_at,
updated_at: job.updated_at,
error: job.error
}}
else
{:error, :unauthorized} = error -> error
{:error, :not_found} -> {:error, :not_found}
end
end
@doc """
Issues a stream subscription for telemetry (GET /v1/jobs/:id/stream).
Returns a function that can be used by WebSocket/SSE handlers to receive
messages via mailbox or enumerator.
"""
@spec stream(request(), String.t()) ::
{:ok, %{subscribe: (-> :ok)}} | {:error, :unauthorized | :not_found}
def stream(%{headers: headers}, job_id) do
with {:ok, _actor} <- Auth.verify(headers),
{:ok, _job} <- JobStore.get(job_id),
{:ok, token} <- extract_stream_token(headers),
:ok <- TelemetryBroker.verify_stream_token(token, job_id) do
{:ok, %{subscribe: fn -> TelemetryBroker.subscribe(job_id) end}}
else
{:error, :unauthorized} = error -> error
{:error, :not_found} -> {:error, :not_found}
_ -> {:error, :unauthorized}
end
end
@doc """
Cancels a running job (POST /v1/jobs/:id/cancel).
"""
@spec cancel(request(), String.t()) :: :ok | {:error, :unauthorized | :not_found}
def cancel(%{headers: headers}, job_id) do
with {:ok, _} <- Auth.verify(headers),
:ok <- JobQueue.cancel(job_id) do
:ok
else
{:error, :unauthorized} = error -> error
{:error, :not_found} -> {:error, :not_found}
end
end
defp extract_stream_token(headers) do
case Enum.find(headers, fn {k, _} -> String.downcase(k) == "x-stream-token" end) do
{_, token} when byte_size(token) > 0 -> {:ok, token}
_ -> {:error, :missing}
end
end
end