fix: allowlist public system input projections
This commit is contained in:
@@ -98,7 +98,8 @@ pub use segment::{
|
||||
pub use segment_log::{LogEntry, RestoredState, SegmentOrigin, SessionExtension, collect_state};
|
||||
pub use store::{Store, StoreError};
|
||||
pub use system_item::{
|
||||
PromptRenderProvenance, SystemItem, SystemReminder, SystemReminderSource, render_worker_event,
|
||||
PromptRenderProvenance, PublicSystemItem, PublicSystemPresentation, SystemItem, SystemReminder,
|
||||
SystemReminderSource, render_worker_event,
|
||||
};
|
||||
pub use uploaded_file::{
|
||||
DEFAULT_MAX_FILES_PER_SUBMISSION, DEFAULT_MAX_SESSION_ARTIFACT_BYTES,
|
||||
|
||||
@@ -1992,16 +1992,10 @@ fn system_entry(
|
||||
provenance: SessionEntryProvenance,
|
||||
derived_from: Vec<String>,
|
||||
) -> SessionSnapshotEntry {
|
||||
let mut data = serde_json::to_value(item).ok();
|
||||
if let Some(serde_json::Value::Object(object)) = data.as_mut() {
|
||||
object.remove("prompt_provenance");
|
||||
}
|
||||
let item_kind = data
|
||||
.as_ref()
|
||||
.and_then(|value| value.get("kind"))
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.unwrap_or("system_item")
|
||||
.to_owned();
|
||||
let public = item.public_item();
|
||||
let data = Some(serde_json::to_value(&public).expect("public system input is serializable"));
|
||||
let item_kind = public.kind;
|
||||
let content = public.body;
|
||||
SessionSnapshotEntry {
|
||||
entry_id,
|
||||
timestamp,
|
||||
@@ -2009,7 +2003,7 @@ fn system_entry(
|
||||
derived_from,
|
||||
data: SessionSnapshotEntryData::SystemItem {
|
||||
item_kind,
|
||||
content: item.history_text(),
|
||||
content,
|
||||
data,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -197,7 +197,58 @@ pub enum SystemItem {
|
||||
},
|
||||
}
|
||||
|
||||
/// Allowlisted presentation data shared by live events and retained snapshots.
|
||||
/// Private Feature payloads and render provenance never cross this boundary.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct PublicSystemItem {
|
||||
pub kind: String,
|
||||
pub body: String,
|
||||
pub presentation: PublicSystemPresentation,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
|
||||
pub enum PublicSystemPresentation {
|
||||
Notification { message: String },
|
||||
WorkerEvent { event: WorkerEvent },
|
||||
Context,
|
||||
TaskReminder,
|
||||
Ignored,
|
||||
}
|
||||
|
||||
impl SystemItem {
|
||||
pub fn public_item(&self) -> PublicSystemItem {
|
||||
let presentation = match self {
|
||||
Self::Notification { message, .. } => PublicSystemPresentation::Notification {
|
||||
message: message.clone(),
|
||||
},
|
||||
Self::WorkerEvent { event, .. } => PublicSystemPresentation::WorkerEvent {
|
||||
event: event.clone(),
|
||||
},
|
||||
Self::TaskReminder { .. } => PublicSystemPresentation::TaskReminder,
|
||||
Self::LegacyKnowledgeIgnored { .. } | Self::LegacyIgnored { .. } => {
|
||||
PublicSystemPresentation::Ignored
|
||||
}
|
||||
Self::JournalStartup { .. }
|
||||
| Self::ResidentSummaryRefresh { .. }
|
||||
| Self::SubjectBehaviorRefresh { .. }
|
||||
| Self::SkillActivation { .. }
|
||||
| Self::FeatureInvocationResult { .. }
|
||||
| Self::FileAttachment { .. }
|
||||
| Self::Interrupt { .. } => PublicSystemPresentation::Context,
|
||||
};
|
||||
PublicSystemItem {
|
||||
kind: self.kind_label().to_owned(),
|
||||
body: if matches!(presentation, PublicSystemPresentation::Ignored) {
|
||||
String::new()
|
||||
} else {
|
||||
self.history_text()
|
||||
},
|
||||
presentation,
|
||||
}
|
||||
}
|
||||
|
||||
/// Free-text body the LLM sees inside its `role:system` message
|
||||
/// for this item. Returns the variant's stored `body` verbatim.
|
||||
pub fn history_text(&self) -> String {
|
||||
@@ -311,6 +362,31 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn public_system_input_decoder_rejects_private_fields_and_unknown_presentations() {
|
||||
let valid = serde_json::json!({
|
||||
"kind": "new-feature-label",
|
||||
"body": "explicit public content",
|
||||
"presentation": { "kind": "context" },
|
||||
});
|
||||
let public: PublicSystemItem = serde_json::from_value(valid.clone()).unwrap();
|
||||
assert_eq!(public.body, "explicit public content");
|
||||
for private_field in [
|
||||
"payload",
|
||||
"prompt_provenance",
|
||||
"policy",
|
||||
"snapshot",
|
||||
"behavior_md",
|
||||
] {
|
||||
let mut malformed = valid.clone();
|
||||
malformed[private_field] = serde_json::json!({ "private": true });
|
||||
assert!(serde_json::from_value::<PublicSystemItem>(malformed).is_err());
|
||||
}
|
||||
let mut unknown = valid;
|
||||
unknown["presentation"] = serde_json::json!({ "kind": "execute_feature_payload" });
|
||||
assert!(serde_json::from_value::<PublicSystemItem>(unknown).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn frozen_subject_behavior_log_reads_old_counter_without_republishing_it() {
|
||||
let old = r#"{"kind":"subject_behavior_refresh","body":"exact archived body","behavior_revision":7}"#;
|
||||
|
||||
Reference in New Issue
Block a user