fix(workflows): execute immutable signed revisions

Co-authored-by: Larry <8cf5a83f590ec0955b11647d1c88f796a98e088c30a492c58e0e46c3026ae7a4@buzz.block.builderlab.xyz>
Signed-off-by: Logan Johnson <loganj@squareup.com>
This commit is contained in:
Logan Johnson
2026-08-24 23:21:40 -04:00
parent e8ec3c8b4d
commit 5dc8e4750d
8 changed files with 268 additions and 152 deletions

View File

@@ -44,7 +44,9 @@ use std::collections::HashMap;
use std::sync::Arc;
use std::sync::OnceLock;
use buzz_core::kind::{event_kind_u32, is_workflow_execution_kind, KIND_REACTION};
use buzz_core::kind::{
event_kind_u32, is_workflow_execution_kind, KIND_REACTION, KIND_WORKFLOW_DEF,
};
use buzz_core::tenant::CommunityId;
use buzz_db::workflow::RunStatus;
use buzz_db::Db;
@@ -122,6 +124,62 @@ impl WorkflowEngine {
}
}
/// Load and verify the exact owner-signed definition bound to a run.
///
/// The mutable `workflows` row supplies only immutable identity/channel
/// binding. Definition content always comes from the run's signed event;
/// legacy runs without a revision fail closed.
pub async fn load_run_definition(
&self,
community_id: CommunityId,
run_id: Uuid,
) -> Result<(buzz_db::workflow::WorkflowRunRecord, WorkflowDef), WorkflowError> {
let run = self.db.get_workflow_run(community_id, run_id).await?;
let revision = run.definition_event_id.as_deref().ok_or_else(|| {
WorkflowError::InvalidDefinition(
"workflow run has no owner-signed definition revision".into(),
)
})?;
let workflow = self.db.get_workflow(community_id, run.workflow_id).await?;
let stored = self
.db
.get_event_by_id(community_id, revision)
.await?
.ok_or_else(|| {
WorkflowError::InvalidDefinition(
"workflow run definition event is unavailable".into(),
)
})?;
let event = &stored.event;
let workflow_id = run.workflow_id.to_string();
let channel_id = workflow.channel_id.map(|id| id.to_string());
let exact_tag = |name: &str| {
let mut values = event.tags.iter().filter_map(|tag| {
(tag.kind().to_string() == name)
.then(|| tag.content())
.flatten()
});
let value = values.next();
value.filter(|_| values.next().is_none())
};
if event.id.as_bytes() != revision
|| !event.verify_id()
|| !event.verify_signature()
|| event_kind_u32(event) != KIND_WORKFLOW_DEF
|| event.pubkey.to_bytes().as_slice() != workflow.owner_pubkey
|| exact_tag("d") != Some(workflow_id.as_str())
|| channel_id.is_none()
|| exact_tag("h") != channel_id.as_deref()
|| stored.channel_id != workflow.channel_id
{
return Err(WorkflowError::InvalidDefinition(
"workflow run definition event binding mismatch".into(),
));
}
let (definition, _) = Self::parse_yaml(&event.content)?;
Ok((run, definition))
}
/// Drop the cached enabled-workflow list for a channel.
///
/// Must be called after any write to a workflow's trigger eligibility or
@@ -407,6 +465,7 @@ impl WorkflowEngine {
.create_workflow_run(
community_id,
workflow.id,
workflow.definition_event_id.as_deref(),
Some(&trigger_event_id_bytes),
Some(&trigger_ctx_json),
)
@@ -426,13 +485,22 @@ impl WorkflowEngine {
);
let engine = Arc::clone(self);
let def_clone = def.clone();
let ctx_clone = trigger_ctx.clone();
tokio::spawn(async move {
let result =
executor::execute_run(&engine, community_id, run_id, &def_clone, &ctx_clone)
.await;
let result = match engine.load_run_definition(community_id, run_id).await {
Ok((_, definition)) => {
executor::execute_run(
&engine,
community_id,
run_id,
&definition,
&ctx_clone,
)
.await
}
Err(error) => Err((error, PartialProgress::default())),
};
engine
.finalize_run(community_id, run_id, result, None)
.await;
@@ -669,6 +737,7 @@ impl WorkflowEngine {
.create_workflow_run(
community_id,
workflow.id,
workflow.definition_event_id.as_deref(),
None, // no trigger event for cron
trigger_ctx_json.as_ref(),
)
@@ -721,17 +790,21 @@ impl WorkflowEngine {
);
let engine = Arc::clone(self);
let def_clone = def.clone();
let ctx_clone = trigger_ctx.clone();
tokio::spawn(async move {
let result = executor::execute_run(
&engine,
community_id,
run_id,
&def_clone,
&ctx_clone,
)
.await;
let result = match engine.load_run_definition(community_id, run_id).await {
Ok((_, definition)) => {
executor::execute_run(
&engine,
community_id,
run_id,
&definition,
&ctx_clone,
)
.await
}
Err(error) => Err((error, PartialProgress::default())),
};
engine
.finalize_run(community_id, run_id, result, None)
.await;