blob: 1156b4ac230223b84eeea688f573bb45c45fa280 [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::{ArchiveError, target};
use crate::{
EventLog, StoreError,
context::{ArchivedToolResult, read, selection::Selection},
sequence_number,
};
use maka_agent_facts::archive::ArchiveIdentity;
use maka_agent_facts::archive::ArchivedPlaceholder;
use maka_agent_facts::archive::encode_projection;
use maka_agent_facts::archive::projection_digest;
use maka_agent_facts::event::Fact;
use maka_agent_facts::event::RuntimeEvent;
use sha2::{Digest, Sha256};
use sqlx::{Connection, SqliteConnection};
pub(super) fn validate(
target: &target::Target,
placeholder: &ArchivedPlaceholder,
) -> Result<Vec<u8>, StoreError> {
placeholder.validate().map_err(|_| ArchiveError::Corrupt)?;
target
.projection
.validate(&target.invocation.session_id)
.map_err(|_| ArchiveError::Corrupt)?;
let identity = &placeholder.identity;
if identity.runtime_event_id != target.event_id
|| identity.tool_call_id != target.call.tool_call_id
|| identity.tool_name != target.name
{
return Err(ArchiveError::SourceMismatch.into());
}
let body = encode_projection(&target.projection).map_err(|_| ArchiveError::Corrupt)?;
if body.len() as u64 != identity.original_bytes {
return Err(ArchiveError::SizeMismatch.into());
}
if projection_digest(&target.projection).map_err(|_| ArchiveError::Corrupt)?
!= identity.source_projection_digest
|| hex::encode(Sha256::digest(&body)) != identity.body_sha256
{
return Err(ArchiveError::Corrupt.into());
}
let expected = ArchivedPlaceholder::prepare(
target.event_id.clone(),
target.call.tool_call_id.clone(),
target.name.clone(),
&target.projection,
)
.map_err(|_| ArchiveError::Corrupt)?
.ok_or(ArchiveError::Corrupt)?;
if expected != *placeholder {
return Err(ArchiveError::Corrupt.into());
}
Ok(body)
}
pub(crate) async fn validate_append(
connection: &mut SqliteConnection,
event: &RuntimeEvent,
) -> Result<(), StoreError> {
let Fact::ToolResultArchived { placeholder } = &event.fact else {
return Ok(());
};
let target = target::read(
connection,
&event.invocation.session_id,
&placeholder.identity.runtime_event_id,
)
.await?
.ok_or(ArchiveError::SourceMismatch)?;
validate(&target, placeholder)?;
if selected(connection, &event.invocation.session_id, &target.event_id)
.await?
.is_some()
{
return Err(StoreError::EventConflict);
}
let selection = Selection::for_invocation(connection, &event.invocation).await?;
validate_selection(connection, &selection, &target.event_id).await?;
let baseline = read::latest_selected(
connection,
&selection,
i64::MAX as u64,
&mut crate::context::cache::Verification::default(),
)
.await?;
if baseline
.as_ref()
.is_some_and(|b| target.sequence <= b.checkpoint.covered_through)
{
return Err(StoreError::InvalidTransition(
"archive target is already covered by a checkpoint".into(),
));
}
Ok(())
}
pub(crate) async fn verify_replay(
connection: &mut SqliteConnection,
event: &RuntimeEvent,
) -> Result<(), StoreError> {
if let Fact::ToolResultArchived { placeholder } = &event.fact {
let (_, accepted) = find(
connection,
&event.invocation.session_id,
&placeholder.identity.runtime_event_id,
i64::MAX as u64,
)
.await?
.ok_or(ArchiveError::Corrupt)?;
if accepted != *placeholder {
return Err(StoreError::EventConflict);
}
}
Ok(())
}
pub(crate) async fn find(
connection: &mut SqliteConnection,
session: &str,
target_id: &str,
before: u64,
) -> Result<Option<(u64, ArchivedPlaceholder)>, StoreError> {
let rows: Vec<(i64, Option<String>,String,String)> = sqlx::query_as(
"SELECT sequence, CASE WHEN length(CAST(event_json AS BLOB)) <= 32768 THEN event_json END,event_id,invocation_id
FROM runtime_events WHERE kind = 'tool_result_archived'
AND CAST(json_extract(event_json, '$.fact.placeholder.identity.runtime_event_id') AS TEXT) = ?1 AND sequence < ?2
AND json_extract(event_json, '$.invocation.session_id') = ?3 LIMIT 2",
).bind(target_id).bind(before as i64).bind(session).fetch_all(&mut *connection).await?;
if rows.len() > 1 {
return Err(ArchiveError::Corrupt.into());
}
let Some((sequence, json, event_id, invocation_id)) = rows.into_iter().next() else {
return Ok(None);
};
let event: RuntimeEvent = serde_json::from_str(&json.ok_or(ArchiveError::Corrupt)?)
.map_err(|_| ArchiveError::Corrupt)?;
if event.invocation.session_id != session
|| event.id != event_id
|| event.invocation.invocation_id != invocation_id
{
return Err(ArchiveError::Corrupt.into());
}
let writer:bool=sqlx::query_scalar("SELECT EXISTS(SELECT 1 FROM runtime_events o WHERE o.invocation_id=? AND o.kind='invocation_opened' AND o.sequence<? AND json_extract(o.event_json,'$.invocation')=json(?) AND NOT EXISTS(SELECT 1 FROM runtime_events t WHERE t.invocation_id=o.invocation_id AND t.kind='invocation_ended' AND t.sequence<?))")
.bind(&invocation_id).bind(sequence).bind(serde_json::to_string(&event.invocation)?).bind(sequence).fetch_one(&mut *connection).await?;
if !writer {
return Err(ArchiveError::Corrupt.into());
}
let Fact::ToolResultArchived { placeholder } = event.fact else {
return Err(ArchiveError::Corrupt.into());
};
let target = target::read(connection, session, target_id)
.await?
.ok_or(ArchiveError::Corrupt)?;
if target.sequence >= sequence_number(sequence)? {
return Err(ArchiveError::Corrupt.into());
}
let selection = Selection::for_invocation(connection, &event.invocation).await?;
validate_selection(connection, &selection, target_id).await?;
// Do not recurse into checkpoint proofs: those validate their archive source here.
let baseline =
read::latest_record(connection, &selection, sequence_number(sequence)? - 1).await?;
let covered = match baseline.map(|b| b.event.fact) {
Some(Fact::ContextCheckpointRecorded { checkpoint }) => checkpoint.covered_through,
None => 0,
_ => return Err(ArchiveError::Corrupt.into()),
};
if target.sequence <= covered {
return Err(ArchiveError::Corrupt.into());
}
validate(&target, &placeholder)?;
Ok(Some((sequence as u64, placeholder)))
}
async fn validate_selection(
connection: &mut SqliteConnection,
selection: &Selection,
event_id: &str,
) -> Result<(), StoreError> {
let filter = Selection::predicate("t", "?2");
let visible: bool = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
"SELECT EXISTS(SELECT 1 FROM runtime_events t WHERE t.event_id=?1 AND {filter})"
)))
.bind(event_id)
.bind(&selection.lineage)
.fetch_one(connection)
.await?;
if !visible {
return Err(ArchiveError::SourceMismatch.into());
}
Ok(())
}
pub(crate) async fn archived(
connection: &mut SqliteConnection,
session: &str,
target_id: &str,
before: u64,
) -> Result<Option<ArchivedToolResult>, StoreError> {
let Some((_, placeholder)) = find(connection, session, target_id, before).await? else {
return Ok(None);
};
// Validation above releases its bounded base; only compact identity survives.
let (sequence, invocation, operation_id, is_error): (i64,String,String,bool) = sqlx::query_as(
"SELECT sequence,json_extract(event_json,'$.invocation'),operation_id,
json_extract(event_json,'$.fact.outcome.kind') = 'failed' FROM runtime_events WHERE event_id = ?",
).bind(target_id).fetch_one(connection).await?;
Ok(Some(ArchivedToolResult {
sequence: sequence_number(sequence)?,
event_id: target_id.into(),
invocation: serde_json::from_str(&invocation)?,
operation_id,
replacement: placeholder
.to_canonical_projection()
.map_err(|_| ArchiveError::Corrupt)?,
is_error,
}))
}
/// Select the owner's archive or its immutable inherited archive, then verify
/// against the actual writer. A copied archive may come from an intermediate copy.
async fn selected(
connection: &mut SqliteConnection,
session: &str,
event_id: &str,
) -> Result<Option<ArchivedPlaceholder>, StoreError> {
let filter = Selection::archive_predicate("t", "a", "9223372036854775807", "NULL");
let rows: Vec<(Option<String>, Option<i64>, Option<i64>)> =
sqlx::query_as(sqlx::AssertSqlSafe(format!(
"SELECT json_extract(a.event_json,'$.invocation.session_id'), a.sequence, t.archive_sequence
FROM session_history_events t LEFT JOIN runtime_events a
ON a.kind='tool_result_archived'
AND CAST(json_extract(a.event_json,'$.fact.placeholder.identity.runtime_event_id') AS TEXT)=t.event_id
AND {filter}
WHERE t.owner_session_id=?1 AND t.event_id=?2 LIMIT 2"
)))
.bind(session)
.bind(event_id)
.fetch_all(&mut *connection)
.await?;
if rows.len() > 1 {
return Err(ArchiveError::Corrupt.into());
}
let Some((writer, sequence, pinned)) = rows.into_iter().next() else {
return Ok(None);
};
let (writer, sequence) = match (writer, sequence, pinned) {
(Some(writer), Some(sequence), _) => (writer, sequence),
(None, None, None) => return Ok(None),
_ => return Err(ArchiveError::Corrupt.into()),
};
let (accepted_sequence, placeholder) = find(
connection,
&writer,
event_id,
sequence_number(sequence)? + 1,
)
.await?
.ok_or(ArchiveError::Corrupt)?;
if accepted_sequence != sequence_number(sequence)? {
return Err(ArchiveError::Corrupt.into());
}
Ok(Some(placeholder))
}
impl EventLog {
/// Read the committed model projection with or without a later archive replacement.
pub async fn read_tool_result(
&self,
session: &str,
event_id: &str,
) -> Result<Option<super::ToolResultResource>, StoreError> {
self.validate_root()?;
crate::sessions::validate_id(session)?;
let (session, event_id) = (session.to_owned(), event_id.to_owned());
self.connection
.run(move |connection| {
Box::pin(async move {
let mut tx = connection.begin().await?;
let Some(mut target) = target::read(&mut tx, &session, &event_id).await? else {
return Ok(None);
};
target
.projection
.validate(&target.invocation.session_id)
.map_err(|_| ArchiveError::Corrupt)?;
if let Some(placeholder) = selected(&mut tx, &session, &event_id).await? {
validate(&target, &placeholder)?;
}
// Verify original evidence before rendering typed media locators.
// Raw archive reads below retain the original digest and bytes.
if target.invocation.session_id != session
&& let maka_agent_facts::tool_output::DurableToolProjection::Content {
parts,
} = &mut target.projection
{
for part in parts {
if let Some(reference) = part.reference_mut() {
let maka_agent_facts::attachment::StorageRef::SessionFile {
session_id,
relative_path,
} = &*reference
else {
return Err(ArchiveError::Corrupt.into());
};
*reference = crate::artifacts::history::resolve(
&mut tx,
&session,
session_id,
relative_path,
)
.await?
.ok_or(ArchiveError::Corrupt)?;
}
}
}
let bytes = maka_agent_facts::archive::encode_projection(&target.projection)
.map_err(|_| ArchiveError::Corrupt)?;
let serialized_result =
String::from_utf8(bytes).map_err(|_| ArchiveError::Corrupt)?;
tx.commit().await?;
Ok(Some(super::ToolResultResource {
tool_name: target.name,
serialized_result,
}))
})
})
.await
}
pub async fn read_archive(
&self,
session: &str,
identity: &ArchiveIdentity,
) -> Result<Option<Vec<u8>>, StoreError> {
identity
.validate()
.map_err(|_| ArchiveError::SourceMismatch)?;
let Some((accepted, bytes)) = self
.read_archive_by_event(session, &identity.runtime_event_id)
.await?
else {
return Ok(None);
};
if accepted != *identity {
return Err(ArchiveError::SourceMismatch.into());
}
Ok(Some(bytes))
}
/// Resolve a locator through accepted evidence in the caller's Session, never raw payload alone.
pub async fn read_archive_by_event(
&self,
session: &str,
event_id: &str,
) -> Result<Option<(ArchiveIdentity, Vec<u8>)>, StoreError> {
self.validate_root()?;
crate::sessions::validate_id(session)?;
let (session, event_id) = (session.to_owned(), event_id.to_owned());
self.connection
.run(move |connection| {
Box::pin(async move {
let mut tx = connection.begin().await?;
let Some(target) = target::read(&mut tx, &session, &event_id).await? else {
return Ok(None);
};
let Some(placeholder) = selected(&mut tx, &session, &event_id).await? else {
return Ok(None);
};
let body = validate(&target, &placeholder)?;
tx.commit().await?;
Ok(Some((placeholder.identity, body)))
})
})
.await
}
}