blob: a90806d3444e1b424fc729fce04f4231daaa191b [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::invalid;
use crate::{
StoreError,
artifacts::references,
bundle::{
Inventory,
format::{Blob, Record},
},
};
use maka_agent_facts::artifact::Artifact;
use maka_agent_facts::attachment::AttachmentRef;
use maka_agent_facts::attachment::StorageRef;
use maka_agent_facts::event::RuntimeEvent;
use sqlx::SqliteConnection;
pub(super) async fn validate(
staged: &mut SqliteConnection,
original: &mut SqliteConnection,
inventory: &Inventory,
positions: Option<&crate::bundle::relocation::Positions>,
) -> Result<(), StoreError> {
sqlx::query("DELETE FROM referenced_mappings")
.execute(&mut *staged)
.await?;
let mut after = 0i64;
loop {
let row: Option<(i64, String)> = sqlx::query_as(
"SELECT number,record_json FROM frames WHERE number>? AND
((kind='blob' AND json_extract(record_json,'$.resource')='artifact') OR kind='history_artifact')
ORDER BY number LIMIT 1"
).bind(after).fetch_optional(&mut *staged).await?;
let Some((number, json)) = row else { break };
match serde_json::from_str(&json)? {
Record::Blob(Blob::Artifact {
metadata,
digest,
frozen_at,
}) => {
selected(inventory, &metadata.session_id)?;
crate::artifacts::validate_material(&metadata, metadata.size_bytes, &digest)?;
if let Some(source) = frozen_at {
let sequence = match positions {
Some(positions) => positions.event(source)?,
None => source,
};
frozen_witness(staged, original, &metadata, sequence).await?;
}
}
Record::HistoryArtifact {
session,
source_session,
source_artifact,
artifact,
} => {
selected(inventory, &session)?;
crate::sessions::validate_id(&source_session)?;
maka_agent_facts::interaction::entity_id(&source_artifact).map_err(invalid)?;
let identity = maka_agent_facts::artifact::content_digest(&serde_json::to_vec(&(
&source_session,
&source_artifact,
))?);
if artifact != format!("history-{}", identity.trim_start_matches("sha256:")) {
return Err(invalid(
"bundle history Artifact identity differs from its provenance",
));
}
if descriptor(staged, &session, &artifact).await?.is_none() {
return Err(invalid("bundle history mapping lacks its owned Artifact"));
}
}
_ => unreachable!(),
}
after = number;
}
for session in &inventory.sessions {
let mut after = (0i64, false);
loop {
// Bound each ownership branch before merging so every cursor step
// sorts at most three candidates instead of the remaining history.
let row: Option<(i64, bool, bool, String)> = sqlx::query_as(
"SELECT sequence,revision,inherited,event_json FROM (
SELECT e.sequence,0 AS revision,0 AS inherited,e.event_json FROM runtime_events e
WHERE (e.event_json IS NOT NULL AND e.event_session=?1) AND e.sequence>?2
ORDER BY e.sequence LIMIT 1)
UNION ALL
SELECT sequence,revision,inherited,event_json FROM (
SELECT e.sequence,0 AS revision,1 AS inherited,e.event_json FROM session_history_members h
JOIN runtime_events e ON e.sequence=h.sequence WHERE h.session_id=?1 AND h.sequence>?2
ORDER BY h.sequence LIMIT 1)
UNION ALL
SELECT sequence,revision,inherited,event_json FROM (
SELECT r.sequence,1 AS revision,1 AS inherited,e.event_json FROM session_revision_sources r
JOIN runtime_events e ON e.sequence=r.sequence
WHERE r.session_id=?1 AND r.sequence>=?2 AND (r.sequence>?2 OR ?3=0)
ORDER BY r.sequence LIMIT 1)
ORDER BY sequence,revision LIMIT 1"
).bind(&session.id).bind(after.0).bind(after.1).fetch_optional(&mut *original).await?;
let Some((sequence, revision, inherited, json)) = row else {
break;
};
let event: RuntimeEvent = serde_json::from_str(&json)?;
let refs = if revision {
references::revision(&event)?
.into_iter()
.map(|a| (&a.storage_ref, Some(a)))
.collect()
} else {
references::from_event(&event)
};
for (reference, attachment) in refs {
reference_material(staged, &session.id, inherited, reference, attachment).await?;
}
generated(staged, &session.id, inherited, &event).await?;
after = (sequence, revision);
}
}
let unbound: bool = sqlx::query_scalar(
"SELECT EXISTS(SELECT 1 FROM frames f WHERE f.kind='history_artifact'
AND NOT EXISTS(SELECT 1 FROM referenced_mappings r WHERE r.frame=f.number))",
)
.fetch_one(staged)
.await?;
if unbound {
return Err(invalid(
"bundle history mapping lacks a typed copied reference",
));
}
Ok(())
}
async fn frozen_witness(
staged: &mut SqliteConnection,
original: &mut SqliteConnection,
artifact: &Artifact,
sequence: u64,
) -> Result<(), StoreError> {
use maka_agent_facts::artifact::ArtifactKind;
use maka_agent_facts::event::Fact;
use maka_agent_facts::event::ToolOutcome;
use maka_agent_facts::tool_output::DurableToolProjection;
if sequence == 0 || sequence > i64::MAX as u64 {
return Err(invalid("invalid bundle media witness position"));
}
// A proof-only event's existence is insufficient. Membership belongs to the
// exact artifact owner; a copied owner resolves only its validated mapping.
let row: Option<(bool, String)> = sqlx::query_as(
"SELECT inherited,event_json FROM session_history_events WHERE owner_session_id=? AND sequence=?",
).bind(&artifact.session_id).bind(sequence as i64).fetch_optional(&mut *original).await?;
let (inherited, json) =
row.ok_or_else(|| invalid("bundle media witness is outside its owner history"))?;
let event: RuntimeEvent = serde_json::from_str(&json)?;
let Fact::ToolSettled {
outcome:
ToolOutcome::Succeeded {
model_projection: DurableToolProjection::Content { parts },
..
},
..
} = event.fact
else {
return Err(invalid(
"bundle media witness is not a successful media consumption",
));
};
for part in parts {
let Some((
StorageRef::SessionFile {
session_id,
relative_path,
},
mime,
)) = part.media()
else {
continue;
};
if artifact.mime_type.as_deref() != Some(mime)
|| artifact.kind
!= if mime.starts_with("image/") {
ArtifactKind::Image
} else {
ArtifactKind::File
}
{
continue;
}
let matches = if inherited {
sqlx::query_scalar::<_, bool>(
"SELECT EXISTS(SELECT 1 FROM frames WHERE kind='history_artifact'
AND json_extract(record_json,'$.session')=? AND json_extract(record_json,'$.artifact')=?
AND json_extract(record_json,'$.source_session')=? AND json_extract(record_json,'$.source_artifact')=?)",
).bind(&artifact.session_id).bind(&artifact.id).bind(session_id).bind(relative_path)
.fetch_one(&mut *staged).await?
} else {
session_id == &artifact.session_id && relative_path == &artifact.id
};
if matches {
return Ok(());
}
}
Err(invalid(
"bundle media witness does not bind its owned material",
))
}
async fn reference_material(
staged: &mut SqliteConnection,
owner: &str,
inherited: bool,
reference: &StorageRef,
attachment: Option<&AttachmentRef>,
) -> Result<(), StoreError> {
let StorageRef::SessionFile {
session_id,
relative_path,
} = reference
else {
// Workspace and external files are not transferred, and Session context
// is canonical evidence rather than an Artifact payload.
return Ok(());
};
let artifact = if inherited {
let mapping: Option<(i64, String)> = sqlx::query_as(
"SELECT number,json_extract(record_json,'$.artifact') FROM frames WHERE kind='history_artifact'
AND json_extract(record_json,'$.session')=? AND json_extract(record_json,'$.source_session')=?
AND json_extract(record_json,'$.source_artifact')=?"
).bind(owner).bind(session_id).bind(relative_path).fetch_optional(&mut *staged).await?;
let (frame, id) = mapping
.ok_or_else(|| invalid("bundle copied reference lacks its retained Artifact"))?;
sqlx::query("INSERT INTO referenced_mappings(frame) VALUES(?) ON CONFLICT DO NOTHING")
.bind(frame)
.execute(&mut *staged)
.await?;
descriptor(staged, owner, &id)
.await?
.ok_or_else(|| invalid("bundle history mapping lacks its owned Artifact"))?
.0
} else if session_id == owner {
// Users may delete ordinary uploads while their immutable references
// remain in history. Preserve that absence instead of inventing bytes.
let Some((artifact, _)) = descriptor(staged, owner, relative_path).await? else {
return Ok(());
};
artifact
} else {
return Ok(());
};
if let Some(attachment) = attachment {
artifact.validate_attachment(attachment).map_err(invalid)?;
}
Ok(())
}
async fn descriptor(
staged: &mut SqliteConnection,
session: &str,
id: &str,
) -> Result<Option<(Artifact, String)>, StoreError> {
let row: Option<String> = sqlx::query_scalar(
"SELECT record_json FROM frames WHERE kind='blob' AND json_extract(record_json,'$.resource')='artifact'
AND json_extract(record_json,'$.metadata.sessionId')=? AND json_extract(record_json,'$.metadata.id')=?"
).bind(session).bind(id).fetch_optional(staged).await?;
row.map(|json| {
let Record::Blob(Blob::Artifact {
metadata, digest, ..
}) = serde_json::from_str(&json)?
else {
unreachable!()
};
Ok((metadata, digest))
})
.transpose()
}
async fn generated(
staged: &mut SqliteConnection,
owner: &str,
inherited: bool,
event: &RuntimeEvent,
) -> Result<(), StoreError> {
use maka_agent_facts::artifact::ArtifactKind;
use maka_agent_facts::artifact::ArtifactSource;
use maka_agent_facts::event::Fact;
use maka_agent_facts::event::ToolOutcome;
use maka_agent_facts::tool_output::DurableToolProjection;
let Fact::ToolSettled {
outcome:
ToolOutcome::Succeeded {
artifacts,
model_projection,
..
},
..
} = &event.fact
else {
return Ok(());
};
for evidence in artifacts {
let id = if inherited {
sqlx::query_scalar(
"SELECT json_extract(record_json,'$.artifact') FROM frames WHERE kind='history_artifact'
AND json_extract(record_json,'$.session')=? AND json_extract(record_json,'$.source_session')=?
AND json_extract(record_json,'$.source_artifact')=?"
).bind(owner).bind(&event.invocation.session_id).bind(&evidence.id)
.fetch_optional(&mut *staged).await?
.ok_or_else(|| invalid("bundle generated Artifact has no copied ownership"))?
} else {
evidence.id.clone()
};
let (artifact, digest) = descriptor(staged, owner, &id)
.await?
.ok_or_else(|| invalid("bundle lacks generated tool material"))?;
let mime_matches = matches!(model_projection, DurableToolProjection::Content { parts } if parts.iter().any(|part|
part.media().is_some_and(|(reference, mime)|
matches!(reference, StorageRef::SessionFile { relative_path, .. } if relative_path == &evidence.id)
&& artifact.mime_type.as_deref() == Some(mime)
&& artifact.kind == if mime.starts_with("image/") { ArtifactKind::Image } else { ArtifactKind::File })
));
if artifact.source != ArtifactSource::ToolResultProjection
|| artifact.turn_id != event.invocation.turn_id
|| artifact.size_bytes != evidence.bytes
|| digest != evidence.digest
|| !mime_matches
{
return Err(invalid(
"bundle generated tool material differs from its canonical evidence",
));
}
}
Ok(())
}
fn selected(inventory: &Inventory, id: &str) -> Result<(), StoreError> {
if inventory
.sessions
.binary_search_by(|s| s.id.as_str().cmp(id))
.is_err()
{
return Err(invalid("bundle Artifact is outside its selected catalog"));
}
Ok(())
}