Share ApiSession between quire-ci and quire-server
Pull the run_id + server_url + auth_token tuple into
quire_core::ci::transport so both sides exchange one struct
instead of two parallel definitions. QUIRE_CI_TOKEN is now
required when --transport api — the token is non-optional once
shared.
Assisted-by: Owl Alpha via pi
diff --git a/quire-ci/src/main.rs b/quire-ci/src/main.rs
index bd4fbc5..a33495a 100644
--- a/quire-ci/src/main.rs
+++ b/quire-ci/src/main.rs
@@ -12,6 +12,7 @@ use quire_core::ci::event::{Event, EventKind, JobOutcome};
use quire_core::ci::pipeline::{self, Pipeline, RunFn};
use quire_core::ci::run::RunMeta;
use quire_core::ci::runtime::{Runtime, RuntimeError, RuntimeEvent, RuntimeHandle};
+use quire_core::ci::transport::ApiSession;
use quire_core::fennel::FennelError;
use quire_core::telemetry::{self, FmtMode, MietteLayer};
@@ -111,19 +112,25 @@ struct TransportFlags {
impl TransportFlags {
/// Promote into the resolved [`TransportArgs`], folding in the
/// `QUIRE_CI_TOKEN` env var. clap's `required_if_eq` guarantees
- /// the unwraps are safe for `Transport::Api`.
- fn resolve(self, auth_token: Option<String>) -> TransportArgs {
+ /// `--run-id` and `--server-url` are present for `Transport::Api`;
+ /// the token must arrive via env and is non-optional on the wire.
+ fn resolve(self, auth_token: Option<String>) -> miette::Result<TransportArgs> {
match self.transport {
- Transport::Filesystem => TransportArgs::Filesystem,
- Transport::Api => TransportArgs::Api {
- run_id: self
- .run_id
- .expect("clap requires --run-id when --transport api"),
- server_url: self
- .server_url
- .expect("clap requires --server-url when --transport api"),
- auth_token,
- },
+ Transport::Filesystem => Ok(TransportArgs::Filesystem),
+ Transport::Api => {
+ let auth_token = auth_token.ok_or_else(|| {
+ miette::miette!("--transport api requires the QUIRE_CI_TOKEN env var")
+ })?;
+ Ok(TransportArgs::Api(ApiSession {
+ run_id: self
+ .run_id
+ .expect("clap requires --run-id when --transport api"),
+ server_url: self
+ .server_url
+ .expect("clap requires --server-url when --transport api"),
+ auth_token,
+ }))
+ }
}
}
}
@@ -202,27 +209,22 @@ fn parse_events_target(s: &str) -> Result<EventsTarget, String> {
/// Transport for CI ↔ server communication.
///
-/// CLI-shape only; the resolved variant (with run_id/server_url
-/// promoted out of their Options) is `TransportArgs`.
+/// CLI-shape only; the resolved variant (carrying the shared
+/// [`ApiSession`]) is `TransportArgs`.
#[derive(Clone, Debug, PartialEq, Eq, clap::ValueEnum)]
pub enum Transport {
Filesystem,
Api,
}
-/// Resolved transport produced by [`TransportFlags::resolve`].
-///
-/// The `Api` variant's fields are wired up ahead of the HTTP client
-/// that will consume them.
+/// Resolved transport produced by [`TransportFlags::resolve`]. The
+/// `Api` variant carries the shared [`ApiSession`] — same shape the
+/// server constructed when it created the run.
#[derive(Debug)]
-#[allow(dead_code)] // fields read by the upcoming API client
+#[allow(dead_code)] // session read by the upcoming API client
enum TransportArgs {
Filesystem,
- Api {
- run_id: String,
- server_url: String,
- auth_token: Option<String>,
- },
+ Api(ApiSession),
}
fn main() -> miette::Result<()> {
@@ -256,7 +258,7 @@ fn main() -> miette::Result<()> {
}
};
let auth_token = std::env::var("QUIRE_CI_TOKEN").ok();
- let transport = transport.resolve(auth_token);
+ let transport = transport.resolve(auth_token)?;
let (git_dir, meta, secrets, sentry_handoff) = match dispatch {
Some(path) => load_dispatch(&path)?,
None => (
diff --git a/quire-core/src/ci/mod.rs b/quire-core/src/ci/mod.rs
index 7b454cf..40a310e 100644
--- a/quire-core/src/ci/mod.rs
+++ b/quire-core/src/ci/mod.rs
@@ -12,3 +12,4 @@ pub mod pipeline;
pub mod registration;
pub mod run;
pub mod runtime;
+pub mod transport;
diff --git a/quire-core/src/ci/transport.rs b/quire-core/src/ci/transport.rs
new file mode 100644
index 0000000..b27f55a
--- /dev/null
+++ b/quire-core/src/ci/transport.rs
@@ -0,0 +1,22 @@
+//! Shared transport types for CI ↔ server communication.
+//!
+//! The on-the-wire pairing both sides agree on. The orchestrator
+//! constructs an `ApiSession` per run (minting the auth token and
+//! using the run's UUID); quire-ci reconstructs it from CLI flags
+//! plus `QUIRE_CI_TOKEN`.
+
+/// Credentials and endpoint coordinates for a single CI run's API
+/// channel. Holds everything quire-ci needs to call back to the
+/// server about *this* run: which run, where the server is, and
+/// the bearer token it issued.
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub struct ApiSession {
+ /// Run UUID assigned by the orchestrator. Also the value stored
+ /// in `runs.id` server-side.
+ pub run_id: String,
+ /// Base URL of quire-server (e.g. `http://127.0.0.1:3000`).
+ pub server_url: String,
+ /// Bearer token minted at run creation time. Matches
+ /// `runs.auth_token` server-side.
+ pub auth_token: String,
+}
diff --git a/quire-server/src/ci/mod.rs b/quire-server/src/ci/mod.rs
index 5ee76bb..3068146 100644
--- a/quire-server/src/ci/mod.rs
+++ b/quire-server/src/ci/mod.rs
@@ -11,9 +11,10 @@ pub use quire_core::ci::pipeline::{
DefinitionError, Diagnostic, Job, Pipeline, PipelineError, StructureError,
};
pub use quire_core::ci::run::RunMeta;
+pub use quire_core::ci::transport::ApiSession;
pub use quire_core::ci::{pipeline, registration, runtime};
pub use run::{
- ApiTransport, Executor, Run, RunState, Runs, Transport, TransportMode, materialize_workspace,
+ Executor, Run, RunState, Runs, Transport, TransportMode, materialize_workspace,
reconcile_orphans,
};
diff --git a/quire-server/src/ci/run.rs b/quire-server/src/ci/run.rs
index f8790fb..2765ed8 100644
--- a/quire-server/src/ci/run.rs
+++ b/quire-server/src/ci/run.rs
@@ -9,6 +9,7 @@ use std::collections::HashMap;
use std::path::{Path, PathBuf};
use jiff::Timestamp;
+use quire_core::ci::transport::ApiSession;
use quire_core::secret::SecretString;
use rand::{Rng, distr::Alphanumeric};
@@ -41,17 +42,20 @@ pub enum TransportMode {
/// Runtime transport for a single CI run. Built once per run from
/// the config-shape [`TransportMode`] + the top-level `server_url`,
/// then passed to `Runs::create` and `Run::execute_via_quire_ci`.
+/// The `Api` variant carries the shared [`ApiSession`] — quire-ci
+/// receives a structurally identical value via its CLI flags.
#[derive(Clone, Debug, Default)]
pub enum Transport {
#[default]
Filesystem,
- Api(ApiTransport),
+ Api(ApiSession),
}
impl Transport {
/// Build a runtime transport for a new run. For `Api`, mints a
- /// fresh CSPRNG bearer token and pairs it with `server_url`.
- /// Errors if `mode == Api` and `server_url` is missing.
+ /// fresh run ID and CSPRNG bearer token, pairs them with
+ /// `server_url`, and bundles them into an [`ApiSession`]. Errors
+ /// if `mode == Api` and `server_url` is missing.
pub fn for_new_run(mode: TransportMode, server_url: Option<&str>) -> Result<Self> {
match mode {
TransportMode::Filesystem => Ok(Transport::Filesystem),
@@ -59,7 +63,8 @@ impl Transport {
let server_url = server_url
.ok_or(Error::ApiTransportMissingServerUrl)?
.to_string();
- Ok(Transport::Api(ApiTransport {
+ Ok(Transport::Api(ApiSession {
+ run_id: uuid::Uuid::now_v7().to_string(),
server_url,
auth_token: mint_auth_token(),
}))
@@ -68,15 +73,6 @@ impl Transport {
}
}
-/// Runtime config for the API transport, produced at run creation.
-#[derive(Clone, Debug, PartialEq, Eq)]
-pub struct ApiTransport {
- /// Base URL of quire-server (e.g. `http://127.0.0.1:3000`).
- pub server_url: String,
- /// Bearer token for this run, minted at creation time.
- pub auth_token: String,
-}
-
/// The state of a CI run.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum RunState {
@@ -147,15 +143,16 @@ impl Runs {
/// workspace materialization and log storage.
///
/// `transport` is built by the caller via [`Transport::for_new_run`];
- /// its `Api` auth_token (if any) is persisted in the `auth_token`
- /// column so server-side request handlers can look it up.
+ /// for `Api`, the run's id comes from the `ApiSession` (so quire-ci
+ /// and the DB agree on which run a bearer token belongs to) and
+ /// the token is persisted in `runs.auth_token`. For `Filesystem`,
+ /// a fresh UUID is minted here.
pub fn create(&self, meta: &RunMeta, transport: &Transport) -> Result<Run> {
- let id = uuid::Uuid::now_v7().to_string();
- let workspace_path = self.base_dir.join(&id).join("workspace");
- let auth_token_str = match transport {
- Transport::Filesystem => None,
- Transport::Api(api) => Some(api.auth_token.as_str()),
+ let (id, auth_token_str) = match transport {
+ Transport::Filesystem => (uuid::Uuid::now_v7().to_string(), None),
+ Transport::Api(api) => (api.run_id.clone(), Some(api.auth_token.as_str())),
};
+ let workspace_path = self.base_dir.join(&id).join("workspace");
let db = crate::db::open(&self.db_path)?;
@@ -934,6 +931,10 @@ mod tests {
panic!("expected Api transport");
};
+ // Run id and ApiSession.run_id are the same value — quire-ci
+ // and the orchestrator agree on which run a token belongs to.
+ assert_eq!(run.id(), api.run_id);
+
let conn = crate::db::open(&quire.db_path()).expect("db");
let stored: Option<String> = conn
.query_row(
@@ -959,6 +960,11 @@ mod tests {
"token should be alphanumeric, got {:?}",
api.auth_token
);
+ assert!(
+ uuid::Uuid::parse_str(&api.run_id).is_ok(),
+ "run_id should be a UUID, got {:?}",
+ api.run_id
+ );
}
#[test]