blob: f28049313a5d5e156ab2b8355e6055b8ae0ebb65 [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::event::EventWrite;
use maka_agent_facts::event::Fact;
use maka_agent_facts::event::Invocation;
use maka_agent_facts::event::InvocationOutcome;
use maka_agent_facts::event::RuntimeEvent;
use maka_agent_facts::input::InvocationInput;
use maka_agent_facts::tool_call::ToolCallIdentity;
use maka_event_log::{EventLog, StoreError};
fn opening(session: &str, run: &str, invocation: &str, text: &str) -> RuntimeEvent {
RuntimeEvent::new(
Invocation {
session_id: session.into(),
turn_id: "logical-turn".into(),
run_id: run.into(),
invocation_id: invocation.into(),
},
Fact::InvocationOpened {
configuration: None,
input: InvocationInput::Message {
content: text.into(),
request_fingerprint: None,
source_messages: Vec::new(),
},
},
)
}
async fn append(log: &EventLog, event: RuntimeEvent) {
log.append(&EventWrite::plain(event).unwrap())
.await
.unwrap();
}
#[tokio::test]
async fn exact_run_cuts_bind_identity_and_bytes_without_admitting_unproven_replay() {
let temp = tempfile::tempdir().unwrap();
let path = temp.path().join("runtime.sqlite");
let log = EventLog::open(&path).await.unwrap();
let first = opening("session", "source", "source-invocation", "中文 source");
let first_json = serde_json::to_string(&first).unwrap();
append(&log, first.clone()).await;
let before = log
.run_source("session", "source", None)
.await
.unwrap()
.unwrap();
assert_eq!(before.boundary.invocation, first.invocation);
assert_eq!(before.boundary.high_water, 1);
assert_eq!(before.opening.sequence, 1);
assert_eq!(before.opening.event, first);
assert_eq!(before.last.sequence, 1);
assert_eq!(before.last.event, first);
assert_eq!(
before.boundary.digest,
reference_digest(&first.invocation, &[(1, &first_json)])
);
// The same Run name in another Session cannot enter this source or its evidence.
append(
&log,
opening("other", "source", "unrelated", &"x".repeat(16_384)),
)
.await;
append(
&log,
RuntimeEvent::new(
first.invocation.clone(),
Fact::ToolDispatched {
presentation: None,
title: None,
operation_id: "unsettled".into(),
call: ToolCallIdentity::standalone("call".into()),
name: "write".into(),
input: serde_json::json!({}),
},
),
)
.await;
let active = log
.run_source("session", "source", None)
.await
.unwrap()
.unwrap();
assert_eq!(
active.boundary.high_water, 2,
"Run ordinal, not root ledger position"
);
assert_eq!([active.opening.sequence, active.last.sequence], [1, 3]);
assert_ne!(before.boundary.digest, active.boundary.digest);
let last_json = serde_json::to_string(&active.last.event).unwrap();
assert_eq!(
active.boundary.digest,
reference_digest(&first.invocation, &[(1, &first_json), (3, &last_json)])
);
// Reading immutable evidence does not erase an unresolved effect or declare it safe.
assert!(matches!(
active.last.event.fact,
Fact::ToolDispatched { .. }
));
append(
&log,
RuntimeEvent::new(
first.invocation.clone(),
Fact::InvocationEnded {
outcome: InvocationOutcome::Failed {
model_failure: None,
class: "outcome_unknown".into(),
message: None,
},
},
),
)
.await;
append(
&log,
opening(
"session",
"successor",
"successor-invocation",
"later branch",
),
)
.await;
let latest = log
.run_source("session", "source", None)
.await
.unwrap()
.unwrap();
assert_eq!(latest.boundary.high_water, 3);
assert_eq!(latest.last.sequence, 4);
let terminal_json = serde_json::to_string(&latest.last.event).unwrap();
assert_eq!(
latest.boundary.digest,
reference_digest(
&first.invocation,
&[(1, &first_json), (3, &last_json), (4, &terminal_json)]
)
);
assert_eq!(
log.turn_boundary("session", "logical-turn")
.await
.unwrap()
.unwrap()
.invocation
.run_id,
"successor"
);
assert_eq!(
log.run_boundary("session", "source")
.await
.unwrap()
.unwrap()
.invocation,
first.invocation
);
assert!(
log.run_source("absent", "source", None)
.await
.unwrap()
.is_none()
);
for invalid in [0, 4, 9_007_199_254_740_992, u64::MAX] {
assert!(matches!(
log.run_source("session", "source", Some(invalid)).await,
Err(StoreError::InvalidTransition(_))
));
}
log.close().await.unwrap();
let log = EventLog::open(&path).await.unwrap();
let frozen = log
.run_source("session", "source", Some(2))
.await
.unwrap()
.unwrap();
assert_eq!(frozen.boundary.digest, active.boundary.digest);
assert_eq!(frozen.boundary.high_water, active.boundary.high_water);
assert_eq!(frozen.opening.sequence, active.opening.sequence);
assert_eq!(frozen.opening.event, active.opening.event);
assert_eq!(frozen.last.sequence, active.last.sequence);
assert_eq!(frozen.last.event, active.last.event);
assert_eq!(
log.run_source("session", "source", Some(1))
.await
.unwrap()
.unwrap()
.boundary
.digest,
before.boundary.digest
);
log.close().await.unwrap();
// Source authentication binds stored bytes, not reserialized semantic JSON.
{
let db = rusqlite::Connection::open(&path).unwrap();
db.execute(
"UPDATE event_log SET event_json = event_json || ' ' WHERE event_id = ?",
[&first.id],
)
.unwrap();
}
let log = EventLog::open(&path).await.unwrap();
let changed = log
.run_source("session", "source", Some(1))
.await
.unwrap()
.unwrap();
assert_eq!(changed.opening.event, first);
assert_eq!(changed.boundary.high_water, before.boundary.high_water);
assert_ne!(changed.boundary.digest, before.boundary.digest);
assert_eq!(
changed.boundary.digest,
reference_digest(&first.invocation, &[(1, &format!("{first_json} "))])
);
// Synthetic/legacy duplicate Run identities must be unreadable, not latest-wins.
let successor = log
.run_boundary("session", "successor")
.await
.unwrap()
.unwrap();
append(
&log,
RuntimeEvent::new(
successor.invocation,
Fact::InvocationEnded {
outcome: InvocationOutcome::Completed,
},
),
)
.await;
append(
&log,
opening("session", "source", "ambiguous-invocation", "alias"),
)
.await;
assert!(matches!(
log.run_boundary("session", "source").await,
Err(StoreError::InvalidTransition(_))
));
assert!(matches!(
log.run_source("session", "source", Some(1)).await,
Err(StoreError::InvalidTransition(_))
));
log.close().await.unwrap();
}
// Independent reference framing: source count is local; positions and JSON bytes are canonical.
fn reference_digest(invocation: &Invocation, rows: &[(u64, &str)]) -> String {
use sha2::{Digest, Sha256};
let mut digest = Sha256::new();
digest.update(b"maka.run-prefix.v1\0");
digest.update((rows.len() as u64).to_be_bytes());
let identity = serde_json::to_vec(invocation).unwrap();
digest.update((identity.len() as u64).to_be_bytes());
digest.update(identity);
for (sequence, json) in rows {
digest.update(sequence.to_be_bytes());
digest.update((json.len() as u64).to_be_bytes());
digest.update(json.as_bytes());
}
format!("sha256:{}", hex::encode(digest.finalize()))
}
#[tokio::test]
async fn cumulative_raw_observation_bytes_do_not_limit_source_evidence() {
use maka_agent_facts::event::{LogScope, ModelInterruption};
use maka_agent_facts::model::{ModelEvent, TextKind};
let directory = tempfile::tempdir().unwrap();
let path = directory.path().join("large-source.sqlite");
let log = EventLog::open(&path).await.unwrap();
let first = opening(
"session",
"source",
"source-invocation",
"small effective input",
);
append(&log, first.clone()).await;
let event = |fact| RuntimeEvent::new(first.invocation.clone(), fact);
append(
&log,
event(Fact::ModelRequested {
context_breakdown: None,
item_acceptance: false,
purpose: maka_agent_facts::context::ModelPurpose::Main,
context: None,
checkpoint_event_id: None,
step_id: "step".into(),
model_id: "test".into(),
source_scope: LogScope::Root,
source_high_water: 0,
source_digest: "fixture".into(),
effective_source_digest: None,
input_digest: "input".into(),
route_identity: "route".into(),
}),
)
.await;
append(
&log,
event(Fact::ModelObserved {
step_id: "step".into(),
event: ModelEvent::PartStarted {
id: "part".into(),
text_kind: TextKind::Text,
provider_options: None,
},
}),
)
.await;
let mut raw_observation_bytes = 0;
for _ in 0..9 {
let observation = event(Fact::ModelObserved {
step_id: "step".into(),
event: ModelEvent::PartDelta {
id: "part".into(),
text: "x".into(),
provider_options: Some(serde_json::json!({"evidence":"p".repeat(1024 * 1024)})),
},
});
raw_observation_bytes += serde_json::to_vec(&observation).unwrap().len();
append(&log, observation).await;
}
append(
&log,
event(Fact::ModelInterrupted {
step_id: "step".into(),
status: ModelInterruption::Cancelled,
diagnostic: None,
model_failure: None,
}),
)
.await;
let terminal = event(Fact::InvocationEnded {
outcome: InvocationOutcome::Cancelled {
source: "runtime_cancellation".into(),
},
});
append(&log, terminal.clone()).await;
assert!(raw_observation_bytes > 8 * 1024 * 1024);
assert!(matches!(
log.prefix(100, 8 * 1024 * 1024).await,
Err(StoreError::PrefixTooLarge)
));
let source = log
.run_source("session", "source", None)
.await
.unwrap()
.unwrap();
assert_eq!(source.boundary.high_water, 14);
assert_eq!((source.opening.sequence, source.last.sequence), (1, 14));
assert_eq!(source.opening.event, first);
assert_eq!(source.last.event, terminal);
let effective = log.read_model_context("session", None).await.unwrap();
let canonical: Vec<_> = effective
.tail
.iter()
.map(|item| match item {
maka_event_log::context::ContextEvent::Canonical(stored) => &stored.event,
_ => panic!("interrupted observations must not become effective model items"),
})
.collect();
assert!(serde_json::to_vec(&canonical).unwrap().len() < 4096);
assert!(canonical.iter().any(|event| event.id == first.id));
assert!(
canonical
.iter()
.all(|event| !matches!(event.fact, Fact::ModelObserved { .. }))
);
log.close().await.unwrap();
let log = EventLog::open(&path).await.unwrap();
let reopened = log
.run_source("session", "source", None)
.await
.unwrap()
.unwrap();
assert_eq!(reopened.boundary, source.boundary);
assert_eq!(reopened.opening.event, first);
assert_eq!(reopened.last.event, terminal);
log.close().await.unwrap();
}
#[tokio::test]
async fn source_validates_intermediate_identity_and_rejects_extraneous_material() {
for damage in ["identity", "extraneous material"] {
let directory = tempfile::tempdir().unwrap();
let path = directory.path().join("damaged-source.sqlite");
let log = EventLog::open(&path).await.unwrap();
let first = opening("session", "source", "source-invocation", "original");
append(&log, first.clone()).await;
let dispatched = RuntimeEvent::new(
first.invocation.clone(),
Fact::ToolDispatched {
presentation: None,
title: None,
operation_id: "call".into(),
call: ToolCallIdentity::standalone("call-id".into()),
name: "Read".into(),
input: serde_json::json!({}),
},
);
append(&log, dispatched.clone()).await;
append(
&log,
RuntimeEvent::new(
first.invocation.clone(),
Fact::InvocationEnded {
outcome: InvocationOutcome::Failed {
class: "outcome_unknown".into(),
message: None,
model_failure: None,
},
},
),
)
.await;
let valid = log
.run_source("session", "source", None)
.await
.unwrap()
.unwrap();
assert_eq!(valid.boundary.high_water, 3);
assert_eq!(valid.last.sequence, 3);
log.close().await.unwrap();
{
let db = rusqlite::Connection::open(&path).unwrap();
if damage == "identity" {
db.execute("UPDATE event_log SET event_json=json_set(event_json, '$.invocation.turn_id', 'wrong-turn') WHERE event_id=?", [&dispatched.id]).unwrap();
} else {
db.execute(
"INSERT INTO tool_result_payloads(event_id,payload) VALUES(?,x'00')",
[&dispatched.id],
)
.unwrap();
}
}
let log = EventLog::open(&path).await.unwrap();
assert!(
matches!(
log.run_source("session", "source", None).await,
Err(StoreError::InvalidTransition(_))
),
"{damage}"
);
let before_damage = log
.run_source("session", "source", Some(1))
.await
.unwrap()
.unwrap();
assert_eq!(before_damage.boundary.high_water, 1);
assert_eq!(before_damage.last.event, first);
log.close().await.unwrap();
}
}