blob: 40ad68c943378630e5bd7d1a65be8577c0dde1d5 [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
use super::{boundary, invalid, proof, safety, selection::Selection};
use crate::StoreError;
use maka_agent_facts::context::CheckpointMode;
use maka_agent_facts::context::ModelPurpose;
use maka_agent_facts::context::resolve_model_purpose;
use maka_agent_facts::event::Fact;
use maka_agent_facts::event::InvocationInput;
use maka_agent_facts::event::RuntimeEvent;
use sqlx::SqliteConnection;
pub(super) async fn validate(
connection: &mut SqliteConnection,
event: &RuntimeEvent,
verification: &mut super::cache::Verification,
) -> Result<(), StoreError> {
let Fact::ModelRequested {
purpose,
source_scope,
source_high_water,
source_digest,
model_id,
route_identity,
checkpoint_event_id,
effective_source_digest,
context_breakdown,
..
} = &event.fact
else {
return Ok(());
};
if let Some(breakdown) = context_breakdown {
breakdown.validate().map_err(invalid)?;
}
let (opened, opening) = proof::by_kind(
connection,
&event.invocation.invocation_id,
"invocation_opened",
None,
)
.await?;
let Fact::InvocationOpened { input, .. } = &opening.fact else {
return Err(invalid("missing opening"));
};
if opening.invocation != event.invocation {
return Err(invalid("request opening identity mismatch"));
}
let resolved = resolve_model_purpose(input, *purpose).map_err(invalid)?;
if resolved == ModelPurpose::Main && matches!(input, InvocationInput::Message { .. }) {
safety::summary_boundary(connection, &event.invocation.invocation_id).await?;
if let Some(id) = checkpoint_event_id {
let selection = Selection::for_opening(connection, &opening).await?;
if source_scope != &selection.scope {
return Err(invalid("request checkpoint belongs to another scope"));
}
let (baseline, source) = super::read::latest_with_evidence(
connection,
&selection,
*source_high_water,
verification,
)
.await?;
let baseline = baseline.ok_or_else(|| invalid("missing request baseline"))?;
if baseline.event_id != *id || source.digest != *source_digest {
return Err(invalid("request baseline evidence changed"));
}
// A certified checkpoint still needs fresh archive-writer lineage:
// a later Message can make a previously unique Run ambiguous.
crate::archive::validate_selected(
connection,
&selection,
source.high_water,
i64::MAX as u64,
)
.await?;
}
return Ok(());
}
let selection = Selection::for_opening(connection, &opening).await?;
let session = &event.invocation.session_id;
safety::require_safe(connection, session, Some(&event.invocation.invocation_id)).await?;
if resolved == ModelPurpose::Main {
safety::model_boundary(connection, &event.invocation.invocation_id, i64::MAX as u64)
.await?;
}
let high = if resolved == ModelPurpose::Summary {
let mode = match input {
InvocationInput::ContextCompact { .. } => CheckpointMode::Standalone,
InvocationInput::Message { .. }
| InvocationInput::Continuation { .. }
| InvocationInput::Handoff { .. }
if *source_high_water < opened =>
{
CheckpointMode::PreTurn
}
InvocationInput::Message { .. }
| InvocationInput::Continuation { .. }
| InvocationInput::Handoff { .. } => CheckpointMode::MidTurn {
anchor_event_id: opening.id.clone(),
},
_ => return Err(invalid("invalid summary opening")),
};
boundary::source_fence(
connection,
&selection,
Some(&(opened as i64, opening)),
&mode,
source_high_water
.checked_add(1)
.ok_or_else(|| invalid("source overflow"))?,
)
.await?
} else {
safety::model_source_unchanged(connection, &selection, event).await?;
*source_high_water
};
if source_scope != &selection.scope || high != *source_high_water {
return Err(invalid("model request source changed"));
}
let (baseline, source) =
super::read::latest_with_evidence(connection, &selection, high, verification).await?;
if source.digest != *source_digest {
return Err(invalid("model request source changed"));
}
if baseline.as_ref().map(|b| &b.event_id) != checkpoint_event_id.as_ref() {
return Err(invalid(
"model request omitted or changed the selected baseline",
));
}
crate::archive::validate_summary(
connection,
&source,
effective_source_digest
.as_deref()
.ok_or_else(|| invalid("model request requires effective source evidence"))?,
i64::MAX as u64,
)
.await?;
if resolved == ModelPurpose::Main {
return Ok(());
}
let first_summary = boundary::summary_start(
connection,
&event.invocation.invocation_id,
source_scope,
*source_high_water,
i64::MAX as u64,
)
.await?
.map(|sequence| sequence as i64);
boundary::admit_summary(
connection,
&event.invocation.invocation_id,
first_summary.map(|value| value as u64),
*source_high_water,
)
.await?;
let previous_effective: Option<(i64, Option<String>)> = sqlx::query_as(
"SELECT sequence,json_extract(event_json,'$.fact.effective_source_digest') FROM runtime_events
WHERE sequence = ?",
).bind(first_summary).fetch_optional(&mut *connection).await?;
if let Some((sequence, expected)) = previous_effective {
if expected != *effective_source_digest {
return Err(invalid("summary repair changed effective source"));
}
crate::archive::validate_summary(
connection,
&source,
expected
.as_deref()
.ok_or_else(|| invalid("summary requires effective source evidence"))?,
sequence as u64,
)
.await?;
}
let first: Option<(String, String, i64, String, Option<String>)> = sqlx::query_as(
"SELECT json_extract(event_json, '$.fact.model_id'), json_extract(event_json, '$.fact.route_identity'),
json_extract(event_json, '$.fact.source_high_water'), json_extract(event_json, '$.fact.source_digest'),
json_extract(event_json, '$.fact.checkpoint_event_id') FROM runtime_events
WHERE sequence = ?",
).bind(first_summary).fetch_optional(&mut *connection).await?;
if first.is_some_and(|(m, r, h, d, p)| {
m != *model_id
|| r != *route_identity
|| h as u64 != *source_high_water
|| d != *source_digest
|| p != *checkpoint_event_id
}) {
return Err(invalid("summary repair changed source or route"));
}
Ok(())
}