feat: persist System input policies through public consumers

This commit is contained in:
2026-10-20 02:44:28 +09:00
parent 19514a5d6a
commit bb5c50f9ab
40 changed files with 988 additions and 61 deletions
@@ -371,7 +371,7 @@ fn parse_entry_ref(value: &str) -> Result<SessionEntryRef, ToolError> {
fn parse_kind(value: &str) -> Result<ReferenceKind, ToolError> {
ReferenceKind::parse(value).ok_or_else(|| {
ToolError::InvalidArgument(format!(
"invalid kind {value:?}; expected user, assistant, or tool"
"invalid kind {value:?}; expected user, assistant, system, or tool"
))
})
}
@@ -813,6 +813,7 @@ mod tests {
_ => return None,
};
Some(protocol::SessionSnapshotEntry {
use_policy: None,
entry_id: format!("fake-{index:08}"),
timestamp: index as u64,
provenance: protocol::SessionEntryProvenance::LegacyUnknown,
+1
View File
@@ -1499,6 +1499,7 @@ permission = "write"
is_error: false,
},
metadata: session_store::LoggedSessionHistoryMetadata {
system_input: None,
entry_id: session_store::LoggedSessionHistoryEntryId(entry_id.into()),
origin: session_store::LoggedSessionHistoryOrigin::LegacyUnknown,
derivation: None,
+2 -1
View File
@@ -90,7 +90,7 @@ pub fn live_log_entry_event(entry: LogEntry) -> Option<Event> {
}
LogEntry::AnnotatedSystemItem { entry, .. } => {
let entry_id = Some(entry.metadata.entry_id.0.clone());
let value = serde_json::to_value(entry.item.public_item())
let value = serde_json::to_value(entry.public_item())
.expect("public system input is serializable");
Some(Event::SystemItem {
entry_id,
@@ -348,6 +348,7 @@ mod tests {
prompt_provenance: None,
},
metadata: session_store::LoggedSessionHistoryMetadata {
system_input: None,
entry_id: entry_id.clone(),
origin: session_store::LoggedSessionHistoryOrigin::BackendInstruction {
operation_id: None,
+199 -28
View File
@@ -74,6 +74,7 @@ impl std::fmt::Display for SessionEntryRef {
pub(crate) enum ReferenceKind {
User,
Assistant,
System,
Tool,
}
@@ -82,6 +83,7 @@ impl ReferenceKind {
match self {
Self::User => "user",
Self::Assistant => "assistant",
Self::System => "system",
Self::Tool => "tool",
}
}
@@ -90,6 +92,7 @@ impl ReferenceKind {
match value {
"user" => Some(Self::User),
"assistant" | "agent" => Some(Self::Assistant),
"system" => Some(Self::System),
"tool" => Some(Self::Tool),
_ => None,
}
@@ -131,6 +134,7 @@ pub(crate) struct OverviewItem {
#[derive(Debug, Clone)]
pub(crate) struct ReferenceEntry {
use_policy: Option<protocol::SessionEntryUsePolicy>,
pub id: SessionEntryRef,
pub origin: SessionEntryProvenance,
pub entry_range: [u64; 2],
@@ -242,6 +246,7 @@ pub(crate) struct SessionEntryEvidence {
#[derive(Debug, Clone)]
struct CapturedHistoryEntry {
use_policy: Option<protocol::SessionEntryUsePolicy>,
item: Item,
entry_id: session_store::LoggedSessionHistoryEntryId,
origin: SessionEntryProvenance,
@@ -302,10 +307,12 @@ impl SessionCapture {
is_error,
attachments: _,
} => Item::tool_result_item(call_id, summary, content, is_error),
// Observation deliberately excludes system items and
// controller errors from model-visible session evidence.
SessionSnapshotEntryData::SystemItem { .. }
| SessionSnapshotEntryData::Invoke { .. }
SessionSnapshotEntryData::SystemItem { content, .. } => {
entry.use_policy?;
Item::system_message(content)
}
// Controller errors are not model-visible evidence.
SessionSnapshotEntryData::Invoke { .. }
| SessionSnapshotEntryData::Usage { .. }
| SessionSnapshotEntryData::RunStats { .. }
| SessionSnapshotEntryData::RunYielded { .. }
@@ -314,6 +321,7 @@ impl SessionCapture {
| SessionSnapshotEntryData::RunError { .. } => return None,
};
Some(CapturedHistoryEntry {
use_policy: entry.use_policy,
item,
entry_id: session_store::LoggedSessionHistoryEntryId(entry.entry_id),
origin: entry.provenance,
@@ -328,6 +336,7 @@ impl SessionCapture {
.into_iter()
.enumerate()
.map(|(index, item)| CapturedHistoryEntry {
use_policy: None,
item,
entry_id: session_store::LoggedSessionHistoryEntryId(format!("{index:08}")),
origin: SessionEntryProvenance::LegacyUnknown,
@@ -342,10 +351,21 @@ impl SessionCapture {
) -> Self {
let entries = entries
.into_iter()
.map(|entry| CapturedHistoryEntry {
item: entry.item,
entry_id: entry.annotation.entry_id,
origin: public_provenance(&entry.annotation.origin),
.map(|entry| {
let (item, use_policy) = if let Some(input) = &entry.annotation.system_input {
(
Item::system_message(input.public_projection().body),
Some(input.public_use_policy()),
)
} else {
(entry.item, None)
};
CapturedHistoryEntry {
item,
use_policy,
entry_id: entry.annotation.entry_id,
origin: public_provenance(&entry.annotation.origin),
}
})
.collect();
Self::from_captured_entries(segment_id, entries)
@@ -364,7 +384,8 @@ impl SessionCapture {
let entry_range = [idx as u64, idx as u64];
match item {
Item::Message { role, content, .. } => {
let Some(kind) = message_reference_kind(&entry.origin, role) else {
let Some(kind) = message_reference_kind(&entry.origin, role, entry.use_policy)
else {
continue;
};
let text = content
@@ -376,6 +397,7 @@ impl SessionCapture {
let summary = truncate_chars(&text, 240);
let id = SessionEntryRef::from_history_entry_id(&entry.entry_id);
index.push(ReferenceEntry {
use_policy: entry.use_policy,
id: id.clone(),
origin: entry.origin.clone(),
entry_range,
@@ -386,7 +408,9 @@ impl SessionCapture {
summary: summary.clone(),
search_text: text.clone(),
});
if matches!(kind, ReferenceKind::User | ReferenceKind::Assistant) {
if entry.use_policy.is_none_or(|p| p.observation_readable)
&& matches!(kind, ReferenceKind::User | ReferenceKind::Assistant)
{
overview.push(OverviewItem {
id: id.clone(),
origin: entry.origin.clone(),
@@ -403,6 +427,7 @@ impl SessionCapture {
} => {
let text = format!("{name}\n{arguments}");
index.push(ReferenceEntry {
use_policy: entry.use_policy,
id: SessionEntryRef::from_history_entry_id(&entry.entry_id),
origin: entry.origin.clone(),
entry_range,
@@ -430,6 +455,7 @@ impl SessionCapture {
content.as_deref().unwrap_or_default(),
);
index.push(ReferenceEntry {
use_policy: entry.use_policy,
id: SessionEntryRef::from_history_entry_id(&entry.entry_id),
origin: entry.origin.clone(),
entry_range,
@@ -510,6 +536,9 @@ impl SessionCapture {
let mut hits = Vec::new();
for entry in &self.index {
if entry.use_policy.is_some_and(|p| !p.observation_readable) {
continue;
}
if entry.entry_range[0] < min_entry_index || entry.entry_range[0] > max_entry_index {
continue;
}
@@ -581,6 +610,9 @@ impl SessionCapture {
};
for entry in selected {
if entry.use_policy.is_some_and(|p| !p.observation_readable) {
continue;
}
if entries.len() >= max_items {
truncated = true;
break;
@@ -695,21 +727,11 @@ impl SessionCapture {
pub(crate) fn evidence_for(&self, id: &str) -> Option<SessionEntryEvidence> {
let entry = self.index.iter().find(|entry| entry.id.as_str() == id)?;
let excerpt = self
.read(
ReadSelector::Id(id),
ReadOptions {
include_tools: true,
tool_part: ToolPart::Both,
detail: ReadDetail::Compact,
max_items: 1,
max_bytes: 2 * 1024,
},
)
.entries
.first()
.map(|entry| entry.text.clone())
.unwrap_or_else(|| entry.summary.clone());
if entry.use_policy.is_some_and(|p| !p.memory_evidence) {
return None;
}
let item = &self.entries.get(entry.entry_range[0] as usize)?.item;
let excerpt = render_item(item, entry, ReadDetail::Compact, 2 * 1024);
Some(SessionEntryEvidence {
segment_id: self.segment_id.clone(),
entry_ref: entry.id.clone(),
@@ -731,9 +753,8 @@ fn public_provenance(origin: &WorkerHistoryProvenance) -> SessionEntryProvenance
}
WorkerHistoryProvenance::WorkerInput { .. } => SessionEntryProvenance::WorkerInput,
WorkerHistoryProvenance::FlowInstruction { .. } => SessionEntryProvenance::FlowInstruction,
WorkerHistoryProvenance::BackendInstruction { .. } => {
SessionEntryProvenance::BackendInstruction
}
WorkerHistoryProvenance::BackendInstruction { .. }
| WorkerHistoryProvenance::Provided { .. } => SessionEntryProvenance::BackendInstruction,
WorkerHistoryProvenance::ModelOutput { .. } => SessionEntryProvenance::ModelOutput,
WorkerHistoryProvenance::ToolOutput { .. } => SessionEntryProvenance::ToolOutput,
WorkerHistoryProvenance::CompactionSummary
@@ -746,7 +767,15 @@ fn public_provenance(origin: &WorkerHistoryProvenance) -> SessionEntryProvenance
fn message_reference_kind(
origin: &SessionEntryProvenance,
provider_role: &Role,
use_policy: Option<protocol::SessionEntryUsePolicy>,
) -> Option<ReferenceKind> {
if use_policy.is_some() {
return match provider_role {
Role::System => Some(ReferenceKind::System),
Role::User => Some(ReferenceKind::User),
Role::Assistant => Some(ReferenceKind::Assistant),
};
}
match origin {
SessionEntryProvenance::HumanInput | SessionEntryProvenance::WorkerInput => {
Some(ReferenceKind::User)
@@ -950,6 +979,148 @@ mod tests {
}
}
#[test]
fn generic_public_input_observation_and_memory_policies_survive_store_restore_independently() {
use session_store::{
FeatureInputPayload, FsStore, InputCompactionUse, InputEvidenceUse,
InputObservationUse, LogEntry, LoggedSystemHistoryEntry, PublicSystemItem,
PublicSystemPresentation, Store, SystemInputEnvelope, SystemInputMetadata,
SystemInputUsePolicy,
};
let root = tempfile::tempdir().unwrap();
let store = FsStore::new(root.path()).unwrap();
let session = session_store::new_session_id();
let segment = session_store::new_segment_id();
store
.create_segment(
session,
segment,
&[LogEntry::AnnotatedSegmentStart {
ts: 1,
session_id: session,
system_prompt: None,
config: Default::default(),
history: vec![],
forked_from: None,
compacted_from: None,
}],
)
.unwrap();
let mut inputs = Vec::new();
for (producer, observation, memory_evidence, public) in [
(
"same",
InputObservationUse::Readable,
InputEvidenceUse::Exclude,
true,
),
(
"same",
InputObservationUse::Hidden,
InputEvidenceUse::Include,
true,
),
(
"renamed",
InputObservationUse::Readable,
InputEvidenceUse::Include,
true,
),
(
"renamed",
InputObservationUse::Hidden,
InputEvidenceUse::Exclude,
true,
),
(
"private",
InputObservationUse::Readable,
InputEvidenceUse::Include,
false,
),
] {
let entry = LoggedSystemHistoryEntry::from_input(
WorkerHistoryProvenance::Provided {
producer: producer.into(),
},
None,
SystemInputEnvelope::new(
"private model text",
SystemInputMetadata::new(
SystemInputUsePolicy {
recording_material: InputEvidenceUse::Exclude,
memory_evidence,
observation,
compaction: InputCompactionUse::ReferenceInput,
},
Some(
FeatureInputPayload::encode(
"future:owner",
99,
&serde_json::json!({ "secret": "private metadata" }),
)
.unwrap(),
),
public.then(|| PublicSystemItem {
kind: "supplied-context".into(),
body: "public context: do not extract this".into(),
presentation: PublicSystemPresentation::Context,
}),
)
.unwrap(),
)
.unwrap(),
);
store
.append(
session,
segment,
&LogEntry::AnnotatedSystemItem {
ts: 2,
entry: entry.clone(),
extensions: vec![],
},
)
.unwrap();
inputs.push(entry);
}
let saved = store.read_all(session, segment).unwrap();
let restored =
crate::session_history::restore_history_entries(session, segment, &saved).unwrap();
let snapshot = session_store::public_snapshot::project_session_snapshot(session, &saved);
let snapshot: SessionSnapshot =
serde_json::from_value(serde_json::to_value(snapshot).unwrap()).unwrap();
for capture in [
SessionCapture::from_history_entries(segment.to_string(), restored),
SessionCapture::from_session_snapshot(segment.to_string(), snapshot),
] {
let hits = capture.search(&SearchOptions {
kind: Some(ReferenceKind::System),
..Default::default()
});
assert_eq!(hits.len(), 2);
for (index, input) in inputs.iter().enumerate() {
let reference = SessionEntryRef::from_history_entry_id(&input.metadata.entry_id);
let read =
capture.read(ReadSelector::Id(reference.as_str()), ReadOptions::default());
assert_eq!(!read.entries.is_empty(), matches!(index, 0 | 2));
let evidence = capture.evidence_for(reference.as_str());
assert_eq!(evidence.is_some(), matches!(index, 1 | 2));
for text in read
.entries
.iter()
.map(|entry| entry.text.as_str())
.chain(evidence.iter().map(|entry| entry.excerpt.as_str()))
{
assert!(text.contains("public context: do not extract this"));
assert!(!text.contains("private model text"));
assert!(!text.contains("private metadata"));
assert!(!text.contains("future:owner"));
}
}
}
}
#[test]
fn overview_contains_user_and_assistant_only() {
let view = SessionCapture::new(
+4
View File
@@ -24,6 +24,7 @@ pub(crate) fn test_logged_history_entry(item: impl Into<Item>) -> LoggedHistoryE
LoggedHistoryEntry {
item: session_store::LoggedItem::from(item.into()),
metadata: LoggedSessionHistoryMetadata {
system_input: None,
entry_id: LoggedSessionHistoryEntryId::new(),
origin: LoggedSessionHistoryOrigin::LegacyUnknown,
derivation: None,
@@ -38,6 +39,7 @@ pub(crate) fn test_logged_system_entry(
session_store::LoggedSystemHistoryEntry {
item,
metadata: LoggedSessionHistoryMetadata {
system_input: None,
entry_id: LoggedSessionHistoryEntryId::new(),
origin: LoggedSessionHistoryOrigin::LegacyUnknown,
derivation: None,
@@ -58,6 +60,7 @@ pub(crate) fn metadata(
derivation: Option<SessionHistoryDerivation>,
) -> SessionHistoryMetadata {
SessionHistoryMetadata {
system_input: None,
entry_id: SessionHistoryEntryId::new(),
origin,
derivation,
@@ -80,6 +83,7 @@ pub(crate) fn history_entry_with_id(
HistoryEntry::new(
item,
SessionHistoryMetadata {
system_input: None,
entry_id,
origin,
derivation: None,
+1
View File
@@ -77,6 +77,7 @@ mod tests {
.into_iter()
.enumerate()
.map(|(index, value)| protocol::SessionSnapshotEntry {
use_policy: None,
entry_id: format!("test-{index}"),
timestamp: index as u64,
provenance: protocol::SessionEntryProvenance::LegacyUnknown,
+1
View File
@@ -1338,6 +1338,7 @@ mod tests {
is_error: false,
},
metadata: session_store::LoggedSessionHistoryMetadata {
system_input: None,
entry_id: session_store::LoggedSessionHistoryEntryId(entry_id.into()),
origin: session_store::LoggedSessionHistoryOrigin::LegacyUnknown,
derivation: None,
+1
View File
@@ -5502,6 +5502,7 @@ mod tests {
entry: session_store::LoggedHistoryEntry {
item: session_store::LoggedItem::from(saved),
metadata: session_store::LoggedSessionHistoryMetadata {
system_input: None,
entry_id: session_store::LoggedSessionHistoryEntryId::new(),
origin: session_store::LoggedSessionHistoryOrigin::LegacyUnknown,
derivation: None,
@@ -119,6 +119,7 @@ fn annotated(item: Item) -> session_store::LoggedHistoryEntry {
session_store::LoggedHistoryEntry {
item: session_store::LoggedItem::from(item),
metadata: session_store::LoggedSessionHistoryMetadata {
system_input: None,
entry_id: session_store::LoggedSessionHistoryEntryId::new(),
origin: session_store::LoggedSessionHistoryOrigin::LegacyUnknown,
derivation: None,