blob: 0188bd27715254f000236df9edffa9728376a502 [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 maka_agent_facts::artifact::ArtifactSource;
use maka_agent_facts::event::EventWrite;
use maka_agent_facts::event::Fact;
use maka_agent_facts::event::RuntimeEvent;
use maka_agent_facts::event::ToolOutcome;
use maka_agent_facts::tool_output::ToolOutput;
use maka_agent_facts::tool_output::decode_raw_tool_result;
use rusqlite::{Connection as RawConnection, OptionalExtension, params};
use sqlx::{Connection, SqliteConnection};
use crate::{EventLog, StoreError};
fn invalid() -> StoreError {
StoreError::InvalidTransition("missing or inconsistent durable tool payload".into())
}
/// Metadata validation deliberately does not hydrate or hash historical raw.
pub(crate) fn verify_binding(event: &RuntimeEvent, bytes: Option<i64>) -> Result<(), StoreError> {
match &event.fact {
Fact::ToolSettled {
outcome:
ToolOutcome::Succeeded {
raw,
model_projection,
artifacts,
},
..
} => {
if raw.bytes == 0
|| bytes.and_then(|n| u64::try_from(n).ok()) != Some(raw.bytes)
|| raw.digest.len() != 71
|| !raw.digest.starts_with("sha256:")
|| !raw.digest.as_bytes()[7..].iter().all(u8::is_ascii_hexdigit)
{
return Err(invalid());
}
model_projection
.validate(&event.invocation.session_id)
.map_err(|_| invalid())?;
let mut ids = std::collections::HashSet::new();
let references = crate::artifacts::references::from_event(event);
for artifact in artifacts {
if !ids.insert(&artifact.id)
|| maka_agent_facts::interaction::entity_id(&artifact.id).is_err()
|| artifact.bytes > 9_007_199_254_740_991
|| !maka_agent_facts::archive::valid_projection_digest(&artifact.digest)
|| !references.iter().any(|(reference, _)| {
matches!(reference, maka_agent_facts::attachment::StorageRef::SessionFile { session_id, relative_path }
if session_id == &event.invocation.session_id && relative_path == &artifact.id)
}) {
return Err(invalid());
}
}
}
_ if bytes.is_some() => return Err(invalid()),
_ => {}
}
Ok(())
}
pub(crate) async fn verify_invocation_bindings(
connection: &mut SqliteConnection,
invocation_id: &str,
) -> Result<(), StoreError> {
let broken: bool = sqlx::query_scalar(
"SELECT EXISTS(SELECT 1 FROM runtime_events e
LEFT JOIN tool_result_payloads p ON p.event_id = e.event_id
WHERE e.invocation_id = ? AND e.kind = 'tool_settled'
AND json_extract(e.event_json, '$.fact.outcome.kind') = 'succeeded'
AND (p.event_id IS NULL OR COALESCE(json_extract(e.event_json, '$.fact.outcome.raw.bytes'), 0) != length(p.payload)
OR length(p.payload) = 0))",
).bind(invocation_id).fetch_one(connection).await?;
if broken {
return Err(invalid());
}
Ok(())
}
pub(crate) async fn insert(
connection: &mut SqliteConnection,
write: &EventWrite,
) -> Result<(), StoreError> {
if let Fact::ToolSettled {
outcome: ToolOutcome::Succeeded { artifacts, .. },
..
} = &write.event().fact
&& *artifacts
!= write
.projection_artifacts()
.iter()
.map(|a| a.evidence())
.collect::<Vec<_>>()
{
return Err(invalid());
}
if let Some(payload) = write.raw_payload() {
sqlx::query("INSERT INTO tool_result_payloads (event_id, payload) VALUES (?, ?)")
.bind(&write.event().id)
.bind(payload)
.execute(&mut *connection)
.await?;
}
for pending in write.projection_artifacts() {
let artifact = pending.artifact();
if artifact.source != ArtifactSource::ToolResultProjection
|| artifact.session_id != write.event().invocation.session_id
|| artifact.turn_id != write.event().invocation.turn_id
{
return Err(invalid());
}
crate::artifacts::commit_in_transaction(connection, artifact.clone(), pending.bytes())
.await?;
}
Ok(())
}
pub(crate) async fn verify_replay(
connection: &mut SqliteConnection,
write: &EventWrite,
) -> Result<(), StoreError> {
let equal: Option<bool> =
sqlx::query_scalar("SELECT payload = ? FROM tool_result_payloads WHERE event_id = ?")
.bind(write.raw_payload())
.bind(&write.event().id)
.fetch_optional(&mut *connection)
.await?;
if match write.raw_payload() {
Some(_) => equal != Some(true),
None => equal.is_some(),
} {
return Err(StoreError::EventConflict);
}
for pending in write.projection_artifacts() {
crate::artifacts::verify_projection_replay(connection, pending.artifact(), pending.bytes())
.await?;
}
Ok(())
}
const RAW_META: &str = r#"SELECT e.event_json, length(p.payload) FROM runtime_events e
LEFT JOIN tool_result_payloads p ON p.event_id = e.event_id WHERE e.event_id = ?"#;
const RAW_BYTES: &str =
r#"SELECT payload FROM tool_result_payloads WHERE event_id = ? AND length(payload) = ?"#;
/// Selected T2 only; proof and original payload reads share the caller snapshot.
pub(crate) fn resolve_in_transaction(
db: &RawConnection,
event: &RuntimeEvent,
) -> Result<ToolOutput, StoreError> {
let Fact::ToolSettled {
outcome: ToolOutcome::Succeeded { raw, .. },
..
} = &event.fact
else {
return Err(invalid());
};
let row: Option<(String, Option<i64>)> = db
.prepare_cached(RAW_META)?
.query_row([&event.id], |row| Ok((row.get(0)?, row.get(1)?)))
.optional()?;
let Some((canonical, bytes)) = row else {
return Err(invalid());
};
if serde_json::from_str::<RuntimeEvent>(&canonical)? != *event {
return Err(invalid());
}
verify_binding(event, bytes)?;
let payload: Vec<u8> = db
.prepare_cached(RAW_BYTES)?
.query_row(params![&event.id, raw.bytes as i64], |row| row.get(0))?;
decode_raw_tool_result(&payload, raw).map_err(|_| invalid())
}
impl EventLog {
/// Full raw resolution is bounded and authorized by canonical event + Session.
pub async fn resolve_tool_result(
&self,
session_id: &str,
event_id: &str,
) -> Result<ToolOutput, StoreError> {
self.validate_root()?;
crate::sessions::validate_id(session_id)?;
let (session_id, event_id) = (session_id.to_owned(), event_id.to_owned());
self.connection
.run(move |connection| {
Box::pin(async move {
let mut tx = connection.begin().await?;
let canonical: Option<String> = sqlx::query_scalar(
"SELECT event_json FROM runtime_events WHERE event_id = ? AND kind = 'tool_settled'
AND json_extract(event_json, '$.invocation.session_id') = ?",
).bind(event_id).bind(session_id).fetch_optional(&mut *tx).await?;
let event =
serde_json::from_str::<RuntimeEvent>(&canonical.ok_or_else(invalid)?)?;
let output = tx
.run_on_worker(move |handle| {
// SAFETY: exclusive callback-local non-owning connection;
// raw payload and result are owned, no SQLite object escapes.
let borrowed = unsafe { RawConnection::from_handle(handle.as_ptr()) }?;
borrowed.set_prepared_statement_cache_capacity(100);
resolve_in_transaction(&borrowed, &event)
})
.await??;
tx.commit().await?;
Ok(output)
})
})
.await
}
}