Remove secrets from the bootstrap wire format
Bootstrap no longer carries secrets — quire-ci always resolves them
through the API. Removes the secrets field from Bootstrap, the
write_bootstrap/secrets-revealing logic, the RunContext secrets
pass-through, and the local quire-ci config-loading of secrets.

Assisted-by: Owl Alpha via pi
change lwkxxqtzoupmvonxxzlloysryynvyzpz
commit 401329dea46d755f8a55e1b3e9d7de27ee0ee698
author Alpha Chen <alpha@kejadlen.dev>
date
parent klwwnkuw
diff --git a/quire-ci/src/main.rs b/quire-ci/src/main.rs
index b94f3e2..d4c5e7a 100644
--- a/quire-ci/src/main.rs
+++ b/quire-ci/src/main.rs
@@ -58,7 +58,7 @@ enum Commands {
     ///
     /// `--bootstrap <path>` points at a JSON file (see
     /// [`quire_core::ci::bootstrap::Bootstrap`]) produced by the
-    /// orchestrator that supplies push metadata and secrets.
+    /// orchestrator that supplies push metadata.
     Run {
         /// Where to send the structured event stream. Accepts:
         ///   `null`   — drop events (default).
@@ -76,7 +76,7 @@ enum Commands {
         out_dir: Option<PathBuf>,
 
         /// Path to a JSON bootstrap file produced by the orchestrator.
-        /// Carries push metadata and the secrets the run-fns may resolve.
+        /// Carries push metadata.
         #[arg(long)]
         bootstrap: PathBuf,
 
@@ -564,7 +564,6 @@ fn compile_at(workspace: &std::path::Path) -> Result<Pipeline> {
 #[cfg(test)]
 mod tests {
     use super::*;
-    use std::collections::HashMap;
 
     #[test]
     fn parse_events_target_classifies_input() {
@@ -593,7 +592,6 @@ mod tests {
                 pushed_at: jiff::Timestamp::now(),
             },
             git_dir: PathBuf::from("/tmp/repo.git"),
-            secrets: HashMap::new(),
             sentry: None,
         };
         fs_err::write(&path, serde_json::to_vec(&bootstrap).unwrap()).expect("write");
diff --git a/quire-core/src/ci/bootstrap.rs b/quire-core/src/ci/bootstrap.rs
index ad90dbd..273783f 100644
--- a/quire-core/src/ci/bootstrap.rs
+++ b/quire-core/src/ci/bootstrap.rs
@@ -3,19 +3,14 @@
 //!
 //! The orchestrator writes a [`Bootstrap`] as JSON to a file inside
 //! the run directory and passes the path via `--bootstrap`. `quire-ci`
-//! deserializes it on startup to recover push metadata and the
-//! secrets the run-fns may resolve. Standalone `quire-ci run`
-//! invocations skip the file entirely and fall back to placeholder
-//! values.
+//! deserializes it on startup to recover push metadata. Standalone
+//! `quire-ci run` invocations skip the file entirely and fall back to
+//! placeholder values.
 //!
-//! The file contains live secret values; callers must restrict
-//! permissions (mode 0600 on Unix) before writing. The file is a
-//! one-shot handoff: `quire-ci` unlinks it as soon as it has read
-//! the bytes into memory, and the orchestrator best-effort unlinks
-//! after the subprocess exits as a safety net. Plaintext secrets
-//! should never persist in the run directory.
+//! The file is a one-shot handoff: `quire-ci` unlinks it as soon as
+//! it has read the bytes into memory, and the orchestrator
+//! best-effort unlinks after the subprocess exits as a safety net.
 
-use std::collections::HashMap;
 use std::path::PathBuf;
 
 use serde::{Deserialize, Serialize};
@@ -24,12 +19,6 @@ use crate::ci::run::RunMeta;
 
 /// Inputs the orchestrator supplies to a quire-ci subprocess.
 ///
-/// Secret values cross as plaintext — `SecretString` deliberately
-/// doesn't implement `Serialize` to avoid accidental leaks. The
-/// orchestrator reveals values into this map before writing the
-/// file (mode 0600); quire-ci wraps them back into `SecretString`s
-/// on read.
-///
 /// `git_dir` is the bare repo the run is scoped to. quire-ci surfaces
 /// it via `(jobs :quire/push).git-dir`, which the mirror job's run-fn
 /// passes to git as `GIT_DIR`. The materialized workspace is a flat
@@ -39,7 +28,6 @@ use crate::ci::run::RunMeta;
 pub struct Bootstrap {
     pub meta: RunMeta,
     pub git_dir: PathBuf,
-    pub secrets: HashMap<String, String>,
     /// Sentry handoff, present only when the orchestrator's global
     /// config sets a DSN. Carries the matching trace id so both
     /// sides' events land on the same trace.
@@ -49,8 +37,7 @@ pub struct Bootstrap {
 
 /// What quire-ci needs to mirror the orchestrator's Sentry context.
 ///
-/// The DSN is plaintext like the secrets — the 0600 mode on the
-/// bootstrap file is the line of defense. `trace_id` is the hex form
+/// `trace_id` is the hex form
 /// of [`sentry::protocol::TraceId`]; kept as a string here so
 /// `quire-core` doesn't grow a `sentry` dep.
 #[derive(Debug, Serialize, Deserialize)]
diff --git a/quire-server/src/bin/quire/commands/ci.rs b/quire-server/src/bin/quire/commands/ci.rs
index 93947a1..56d4971 100644
--- a/quire-server/src/bin/quire/commands/ci.rs
+++ b/quire-server/src/bin/quire/commands/ci.rs
@@ -45,11 +45,10 @@ pub async fn run(quire: &Quire, maybe_sha: Option<&str>) -> Result<()> {
     let commit = resolve_commit(maybe_sha)?;
     let ci = Ci::new(repo_path.clone());
 
-    // Pull secrets from the global config; absence is fine for local
-    // testing. A broken-but-present config is a real error.
-    let secrets = match quire.global_config() {
-        Ok(c) => c.secrets,
-        Err(quire::Error::ConfigNotFound(_)) => std::collections::HashMap::new(),
+    // Ensure the global config is valid; absence is fine for local testing.
+    match quire.global_config() {
+        Ok(_) => {}
+        Err(quire::Error::ConfigNotFound(_)) => {}
         Err(e) => return Err(e).into_diagnostic(),
     };
 
@@ -86,14 +85,7 @@ pub async fn run(quire: &Quire, maybe_sha: Option<&str>) -> Result<()> {
     let workspace = tmp.path().join("workspace");
     quire::ci::materialize_workspace(&repo_path.join(".git"), &commit.sha, &workspace)
         .into_diagnostic()?;
-    let exec_result = run.execute(
-        &repo_path.join(".git"),
-        &workspace,
-        &meta,
-        &secrets,
-        None,
-        None,
-    );
+    let exec_result = run.execute(&repo_path.join(".git"), &workspace, &meta, None, None);
 
     // Print the combined quire-ci log regardless of outcome.
     let log_path = tmp.path().join(&run_id).join("quire-ci.log");
diff --git a/quire-server/src/ci/mod.rs b/quire-server/src/ci/mod.rs
index 203c34e..278a60e 100644
--- a/quire-server/src/ci/mod.rs
+++ b/quire-server/src/ci/mod.rs
@@ -1,7 +1,5 @@
 //! CI: trigger runs from push events, validate the job graph.
 
-use std::collections::HashMap;
-
 mod run;
 
 pub(crate) mod error;
@@ -114,7 +112,6 @@ struct TriggerContext<'a> {
 struct RunContext<'a> {
     repo: &'a Repo,
     db_path: &'a Path,
-    secrets: &'a HashMap<String, quire_core::secret::SecretString>,
     executor: Executor,
 }
 
@@ -157,7 +154,6 @@ pub fn trigger(quire: &crate::Quire, event: &PushEvent) {
         run: RunContext {
             repo: &repo,
             db_path: &db_path,
-            secrets: &config.secrets,
             executor: config.ci.executor,
         },
         event_repo: &event.repo,
@@ -262,14 +258,7 @@ fn run_ref_inner(
         Executor::Process => {
             // Compilation happens inside quire-ci so a malformed ci.fnl is
             // reported once, with the worker's trace context.
-            run.execute(
-                &ctx.repo.path(),
-                &workspace,
-                &meta,
-                ctx.secrets,
-                sentry,
-                Some(transport),
-            )?;
+            run.execute(&ctx.repo.path(), &workspace, &meta, sentry, Some(transport))?;
         }
     }
     Ok(())
@@ -417,15 +406,10 @@ mod tests {
         assert!(result.is_err(), "bad Fennel should fail");
     }
 
-    fn run_ctx<'a>(
-        repo: &'a crate::quire::Repo,
-        db_path: &'a std::path::Path,
-        secrets: &'a HashMap<String, quire_core::secret::SecretString>,
-    ) -> RunContext<'a> {
+    fn run_ctx<'a>(repo: &'a crate::quire::Repo, db_path: &'a std::path::Path) -> RunContext<'a> {
         RunContext {
             repo,
             db_path,
-            secrets,
             executor: Executor::Process,
         }
     }
@@ -517,8 +501,7 @@ exit 0
 
         let (_fake_dir, fake_path) = fake_quire_ci(0);
         let db_path = quire.db_path();
-        let secrets = HashMap::new();
-        let ctx = run_ctx(&repo, &db_path, &secrets);
+        let ctx = run_ctx(&repo, &db_path);
         let trigger_result = with_path(&fake_path, || {
             run_ref_inner(
                 &ctx,
@@ -572,8 +555,7 @@ exit 0
 
         let (_fake_dir, fake_path) = fake_quire_ci(1);
         let db_path = quire.db_path();
-        let secrets = HashMap::new();
-        let ctx = run_ctx(&repo, &db_path, &secrets);
+        let ctx = run_ctx(&repo, &db_path);
         let trigger_result = with_path(&fake_path, || {
             run_ref_inner(
                 &ctx,
@@ -617,8 +599,7 @@ exit 0
         };
 
         let db_path = quire.db_path();
-        let secrets = HashMap::new();
-        let ctx = run_ctx(&repo, &db_path, &secrets);
+        let ctx = run_ctx(&repo, &db_path);
         run_ref_inner(
             &ctx,
             pushed_at,
diff --git a/quire-server/src/ci/run.rs b/quire-server/src/ci/run.rs
index 121d53c..0d757d8 100644
--- a/quire-server/src/ci/run.rs
+++ b/quire-server/src/ci/run.rs
@@ -10,7 +10,6 @@ use std::path::{Path, PathBuf};
 
 use jiff::Timestamp;
 use quire_core::ci::transport::ApiSession;
-use quire_core::secret::SecretString;
 use rand::{Rng, distr::Alphanumeric};
 
 use super::error::{Error, Result};
@@ -307,7 +306,6 @@ impl Run {
         git_dir: &Path,
         workspace: &Path,
         meta: &RunMeta,
-        secrets: &HashMap<String, SecretString>,
         sentry: Option<&quire_core::ci::bootstrap::SentryHandoff>,
         transport: Option<&Transport>,
     ) -> Result<()> {
@@ -322,7 +320,7 @@ impl Run {
         let log = fs_err::File::create(&log_path)?.into_parts().0;
         let log_clone = log.try_clone()?;
 
-        write_bootstrap(&bootstrap_path, git_dir, meta, secrets, sentry)?;
+        write_bootstrap(&bootstrap_path, git_dir, meta, sentry)?;
 
         tracing::info!(
             run_id = %self.id,
@@ -628,29 +626,18 @@ impl Run {
 }
 
 /// Serialize the bootstrap payload as JSON and write it to `path` with
-/// owner-only permissions on Unix. Secrets cross as plaintext so the
-/// 0600 mode is the line of defense against other local users; failure
-/// to set the mode aborts the write (better than leaking).
+/// owner-only permissions on Unix.
 fn write_bootstrap(
     path: &Path,
     git_dir: &Path,
     meta: &RunMeta,
-    secrets: &HashMap<String, SecretString>,
     sentry: Option<&quire_core::ci::bootstrap::SentryHandoff>,
 ) -> Result<()> {
     use quire_core::ci::bootstrap::{Bootstrap, SentryHandoff};
 
-    let mut revealed: HashMap<String, String> = HashMap::with_capacity(secrets.len());
-    for (name, value) in secrets {
-        revealed.insert(
-            name.clone(),
-            value.reveal().map_err(Error::Secret)?.to_string(),
-        );
-    }
     let bootstrap = Bootstrap {
         meta: meta.clone(),
         git_dir: git_dir.to_path_buf(),
-        secrets: revealed,
         sentry: sentry.map(|s| SentryHandoff {
             dsn: s.dsn.clone(),
             trace_id: s.trace_id.clone(),
@@ -867,14 +854,7 @@ mod tests {
         let bootstrap_path = dir.path().join("bootstrap.json");
         let git_dir = dir.path().join("repos").join("test.git");
 
-        write_bootstrap(
-            &bootstrap_path,
-            &git_dir,
-            &test_meta(),
-            &HashMap::new(),
-            None,
-        )
-        .expect("write_bootstrap");
+        write_bootstrap(&bootstrap_path, &git_dir, &test_meta(), None).expect("write_bootstrap");
 
         let bytes = fs_err::read(&bootstrap_path).expect("read bootstrap");
         let bootstrap: Bootstrap = serde_json::from_slice(&bytes).expect("parse bootstrap");