fix: reject unrestorable and opaque fixture records
This commit is contained in:
@@ -31,7 +31,11 @@ It does not add a product endpoint or change create/restore authorization.
|
|||||||
|
|
||||||
The template preserves the original Session manifest/JSONL bytes, including stable
|
The template preserves the original Session manifest/JSONL bytes, including stable
|
||||||
entry IDs, timestamps, and settled submission receipts. It rejects resumable queues
|
entry IDs, timestamps, and settled submission receipts. It rejects resumable queues
|
||||||
and Feature invocation history. Portability/secret-marker checks do not replace
|
and Feature invocation history. A saved system prompt is required for restore;
|
||||||
|
checkpoint receipts must match the Worker's typed receipt schema. Preserved records
|
||||||
|
must use canonical writer field shapes: unknown/duplicate fields (including nested
|
||||||
|
fields), discarded optional fields, and opaque Feature history payloads are rejected,
|
||||||
|
not stripped or rewritten. Portability/secret-marker checks do not replace
|
||||||
review of the supplied artificial conversation or its independently verified hashes.
|
review of the supplied artificial conversation or its independently verified hashes.
|
||||||
The installer refuses any destination content other than initialized `auth.toml`;
|
The installer refuses any destination content other than initialized `auth.toml`;
|
||||||
interrupted publication is fail-closed, not an overwrite/recovery facility.
|
interrupted publication is fail-closed, not an overwrite/recovery facility.
|
||||||
|
|||||||
@@ -492,6 +492,7 @@ impl StoppedWorkerTemplate {
|
|||||||
.filter(|line| !line.is_empty())
|
.filter(|line| !line.is_empty())
|
||||||
{
|
{
|
||||||
let _: PortableJson = decode(line)?;
|
let _: PortableJson = decode(line)?;
|
||||||
|
validate_preserved_record(line)?;
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -506,6 +507,18 @@ impl StoppedWorkerTemplate {
|
|||||||
{
|
{
|
||||||
return Err(reject("duplicate or invalid entry identity"));
|
return Err(reject("duplicate or invalid entry identity"));
|
||||||
}
|
}
|
||||||
|
if entry
|
||||||
|
.metadata
|
||||||
|
.input_metadata
|
||||||
|
.as_ref()
|
||||||
|
.is_some_and(|metadata| {
|
||||||
|
metadata.feature_payload().is_some() || metadata.public_item().is_some()
|
||||||
|
})
|
||||||
|
{
|
||||||
|
// These are opaque owner-specific payloads, not part of the narrow
|
||||||
|
// artificial conversation contract. Never preserve unchecked JSON.
|
||||||
|
return Err(reject("Feature history payloads are unsupported"));
|
||||||
|
}
|
||||||
if !matches!(
|
if !matches!(
|
||||||
entry.item,
|
entry.item,
|
||||||
session_store::LoggedItem::Message { .. }
|
session_store::LoggedItem::Message { .. }
|
||||||
@@ -557,10 +570,12 @@ impl StoppedWorkerTemplate {
|
|||||||
.map_err(|_| reject("invalid canonical Session records"))?
|
.map_err(|_| reject("invalid canonical Session records"))?
|
||||||
.0;
|
.0;
|
||||||
if !matches!(logs.first(), Some(session_store::LogEntry::AnnotatedSegmentStart {
|
if !matches!(logs.first(), Some(session_store::LogEntry::AnnotatedSegmentStart {
|
||||||
session_id, forked_from: None, compacted_from: None, ..
|
session_id, system_prompt: Some(_), forked_from: None, compacted_from: None, ..
|
||||||
}) if *session_id == self.session_id)
|
}) if *session_id == self.session_id)
|
||||||
{
|
{
|
||||||
return Err(reject("invalid Segment seed or unsupported lineage"));
|
return Err(reject(
|
||||||
|
"missing system prompt, invalid Segment seed, or unsupported lineage",
|
||||||
|
));
|
||||||
}
|
}
|
||||||
let mut ids = std::collections::BTreeSet::new();
|
let mut ids = std::collections::BTreeSet::new();
|
||||||
for (index, log) in logs.iter().enumerate() {
|
for (index, log) in logs.iter().enumerate() {
|
||||||
@@ -614,6 +629,71 @@ impl StoppedWorkerTemplate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Compare *fields*, not re-encoded bytes or scalar values (e.g. f32 JSON
|
||||||
|
// normalization). Serde's permissive/untagged/flattened compatibility readers
|
||||||
|
// can drop unknown data without surfacing it to an ignored-field callback. A
|
||||||
|
// retained field must survive the owner's typed projection at every depth.
|
||||||
|
// This intentionally accepts only canonical writer shapes: even an explicit
|
||||||
|
// empty optional field omitted by that writer is rejected, not repaired.
|
||||||
|
// The projection is validation-only; installation always writes original bytes.
|
||||||
|
fn validate_preserved_record(bytes: &[u8]) -> Result<(), RuntimeError> {
|
||||||
|
let original: serde_json::Value = decode(bytes)?;
|
||||||
|
let record: session_store::LogEntry = decode(bytes)?;
|
||||||
|
let projection =
|
||||||
|
serde_json::to_value(&record).map_err(|_| reject("invalid typed Session record"))?;
|
||||||
|
require_preserved_fields(&original, &projection)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn require_preserved_fields(
|
||||||
|
original: &serde_json::Value,
|
||||||
|
projection: &serde_json::Value,
|
||||||
|
) -> Result<(), RuntimeError> {
|
||||||
|
use serde_json::Value;
|
||||||
|
match (original, projection) {
|
||||||
|
(Value::Object(original), Value::Object(projection)) => {
|
||||||
|
for (key, value) in original {
|
||||||
|
let projected = projection
|
||||||
|
.get(key)
|
||||||
|
.ok_or_else(|| reject("unknown or noncanonical Session field"))?;
|
||||||
|
require_preserved_fields(value, projected)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(Value::Array(original), Value::Array(projection))
|
||||||
|
if original.len() == projection.len() =>
|
||||||
|
{
|
||||||
|
for (value, projected) in original.iter().zip(projection) {
|
||||||
|
require_preserved_fields(value, projected)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(Value::Null, Value::Null)
|
||||||
|
| (Value::Bool(_), Value::Bool(_))
|
||||||
|
| (Value::Number(_), Value::Number(_))
|
||||||
|
| (Value::String(_), Value::String(_)) => {}
|
||||||
|
_ => return Err(reject("noncanonical Session field shape")),
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exact receipt wire fields owned by worker::PendingActivationState. Keep these
|
||||||
|
// fixture-only mirrors strict; do not change the production compatibility reader.
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
#[serde(deny_unknown_fields)]
|
||||||
|
struct SubmissionReceipt {
|
||||||
|
submission_request_id: String,
|
||||||
|
source_namespace: String,
|
||||||
|
submission_id: String,
|
||||||
|
payload_digest: String,
|
||||||
|
disposition: protocol::SubmissionDisposition,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
#[serde(deny_unknown_fields)]
|
||||||
|
struct NotificationReceipt {
|
||||||
|
notification_request_id: String,
|
||||||
|
source_namespace: String,
|
||||||
|
payload_digest: String,
|
||||||
|
}
|
||||||
|
|
||||||
// The approved conversation has committed submission checkpoints in its original
|
// The approved conversation has committed submission checkpoints in its original
|
||||||
// Session records. Keep their receipt bytes, but reject any resumable queue or
|
// Session records. Keep their receipt bytes, but reject any resumable queue or
|
||||||
// Feature invocation history. This projection confers no execution authority.
|
// Feature invocation history. This projection confers no execution authority.
|
||||||
@@ -630,9 +710,9 @@ fn validate_settled_activation(
|
|||||||
pending: Vec<serde::de::IgnoredAny>,
|
pending: Vec<serde::de::IgnoredAny>,
|
||||||
pending_notifications: Vec<serde::de::IgnoredAny>,
|
pending_notifications: Vec<serde::de::IgnoredAny>,
|
||||||
#[serde(rename = "receipts")]
|
#[serde(rename = "receipts")]
|
||||||
_receipts: Vec<serde::de::IgnoredAny>,
|
_receipts: Vec<SubmissionReceipt>,
|
||||||
#[serde(rename = "notification_receipts")]
|
#[serde(rename = "notification_receipts")]
|
||||||
_notification_receipts: Vec<serde::de::IgnoredAny>,
|
_notification_receipts: Vec<NotificationReceipt>,
|
||||||
invocation_receipts: Vec<serde::de::IgnoredAny>,
|
invocation_receipts: Vec<serde::de::IgnoredAny>,
|
||||||
invocation_recovery_blocked: bool,
|
invocation_recovery_blocked: bool,
|
||||||
}
|
}
|
||||||
@@ -761,7 +841,16 @@ impl<'de> Deserialize<'de> for PortableJson {
|
|||||||
self,
|
self,
|
||||||
mut map: A,
|
mut map: A,
|
||||||
) -> Result<PortableJson, A::Error> {
|
) -> Result<PortableJson, A::Error> {
|
||||||
while map.next_entry::<PortableJson, PortableJson>()?.is_some() {}
|
let mut keys = std::collections::BTreeSet::new();
|
||||||
|
while let Some(key) = map.next_key::<String>()? {
|
||||||
|
// A duplicate key can hide discarded data even inside an
|
||||||
|
// otherwise opaque extension payload. Never retain it.
|
||||||
|
if !keys.insert(key.clone()) {
|
||||||
|
return Err(serde::de::Error::custom("duplicate fixture JSON field"));
|
||||||
|
}
|
||||||
|
serde::de::Visitor::visit_str::<A::Error>(Visitor, &key)?;
|
||||||
|
map.next_value::<PortableJson>()?;
|
||||||
|
}
|
||||||
Ok(PortableJson)
|
Ok(PortableJson)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,52 @@ const RUNTIME: &str = "fixture-runtime";
|
|||||||
const WORKSPACE: &str = "fixture-workspace";
|
const WORKSPACE: &str = "fixture-workspace";
|
||||||
const NEW_URL: &str = "http://127.0.0.1:34567";
|
const NEW_URL: &str = "http://127.0.0.1:34567";
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
#[serde(untagged)]
|
||||||
|
enum ReceiptFixture<T> {
|
||||||
|
Valid(T),
|
||||||
|
Malformed(u8),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Serialize)]
|
||||||
|
struct CheckpointFixture {
|
||||||
|
next_activation_sequence: u64,
|
||||||
|
activating: Option<u8>,
|
||||||
|
activating_notification: Vec<u8>,
|
||||||
|
pending: Vec<u8>,
|
||||||
|
pending_notifications: Vec<u8>,
|
||||||
|
receipts: Vec<ReceiptFixture<SubmissionReceipt>>,
|
||||||
|
notification_receipts: Vec<ReceiptFixture<NotificationReceipt>>,
|
||||||
|
invocation_receipts: Vec<u8>,
|
||||||
|
invocation_recovery_blocked: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn settled_checkpoint() -> CheckpointFixture {
|
||||||
|
CheckpointFixture {
|
||||||
|
next_activation_sequence: 2,
|
||||||
|
receipts: vec![ReceiptFixture::Valid(SubmissionReceipt {
|
||||||
|
submission_request_id: "completed-submission-request".into(),
|
||||||
|
source_namespace: "fixture-source".into(),
|
||||||
|
submission_id: "completed-submission".into(),
|
||||||
|
payload_digest: "fixture-payload-digest".into(),
|
||||||
|
disposition: protocol::SubmissionDisposition::Started,
|
||||||
|
})],
|
||||||
|
notification_receipts: vec![ReceiptFixture::Valid(NotificationReceipt {
|
||||||
|
notification_request_id: "completed-notification-request".into(),
|
||||||
|
source_namespace: "fixture-source".into(),
|
||||||
|
payload_digest: "fixture-notification-digest".into(),
|
||||||
|
})],
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn checkpoint_extension(checkpoint: &CheckpointFixture) -> session_store::SessionExtension {
|
||||||
|
session_store::SessionExtension::new(
|
||||||
|
"worker.pending_activations.v1",
|
||||||
|
serde_json::to_value(checkpoint).unwrap(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
fn worker_id() -> WorkerId {
|
fn worker_id() -> WorkerId {
|
||||||
"01a1657d-b762-7791-87ae-a49f28cadbce".parse().unwrap()
|
"01a1657d-b762-7791-87ae-a49f28cadbce".parse().unwrap()
|
||||||
}
|
}
|
||||||
@@ -185,16 +231,66 @@ fn source() -> (tempfile::TempDir, PathBuf, StoppedWorkerTemplate) {
|
|||||||
LogEntry::AnnotatedSegmentStart {
|
LogEntry::AnnotatedSegmentStart {
|
||||||
ts: 123,
|
ts: 123,
|
||||||
session_id,
|
session_id,
|
||||||
system_prompt: None,
|
system_prompt: Some("Harmless artificial fixture instructions.".into()),
|
||||||
config: Default::default(),
|
config: Default::default(),
|
||||||
history: vec![],
|
history: vec![],
|
||||||
forked_from: None,
|
forked_from: None,
|
||||||
compacted_from: None,
|
compacted_from: None,
|
||||||
},
|
},
|
||||||
LogEntry::AnnotatedAssistantItem {
|
LogEntry::Invoke {
|
||||||
ts: 124,
|
ts: 124,
|
||||||
|
trigger: protocol::InvokeKind::UserSend,
|
||||||
|
},
|
||||||
|
LogEntry::AnnotatedUserInput {
|
||||||
|
ts: 124,
|
||||||
|
segments: vec![protocol::Segment::text(
|
||||||
|
"Harmless artificial fixture input.",
|
||||||
|
)],
|
||||||
|
extensions: vec![checkpoint_extension(&settled_checkpoint())],
|
||||||
|
history: vec![session_store::LoggedHistoryEntry {
|
||||||
|
item: session_store::LoggedItem::Message {
|
||||||
|
role: session_store::LoggedRole::User,
|
||||||
|
content: vec![session_store::LoggedContentPart::Text {
|
||||||
|
text: "Harmless artificial fixture input.".into(),
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
metadata: session_store::LoggedSessionHistoryMetadata {
|
||||||
|
entry_id: session_store::LoggedSessionHistoryEntryId(
|
||||||
|
"fixed-user-id".into(),
|
||||||
|
),
|
||||||
|
origin: session_store::LoggedSessionHistoryOrigin::BackendInstruction {
|
||||||
|
operation_id: None,
|
||||||
|
},
|
||||||
|
derivation: None,
|
||||||
|
input_metadata: None,
|
||||||
|
},
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
LogEntry::AnnotatedAssistantItem {
|
||||||
|
ts: 125,
|
||||||
entry: history,
|
entry: history,
|
||||||
},
|
},
|
||||||
|
LogEntry::LlmUsage {
|
||||||
|
ts: 125,
|
||||||
|
history_len: 2,
|
||||||
|
input_total_tokens: 1,
|
||||||
|
cache_read_tokens: 0,
|
||||||
|
cache_write_tokens: 0,
|
||||||
|
output_tokens: 1,
|
||||||
|
},
|
||||||
|
LogEntry::TurnEnd {
|
||||||
|
ts: 126,
|
||||||
|
turn_count: 1,
|
||||||
|
},
|
||||||
|
LogEntry::RunCompleted {
|
||||||
|
entry_id: Some(session_store::LoggedSessionHistoryEntryId(
|
||||||
|
"fixed-terminal-id".into(),
|
||||||
|
)),
|
||||||
|
ts: 126,
|
||||||
|
interrupted: false,
|
||||||
|
result: agen::EngineResult::Finished,
|
||||||
|
active_run_turn_count: None,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
@@ -554,40 +650,193 @@ fn fresh_manifest_and_memory_settings_are_required() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn settled_checkpoints_preserve_receipts_but_refuse_pending_work() {
|
fn settled_checkpoints_preserve_receipts_but_refuse_pending_work() {
|
||||||
#[derive(Default, Serialize)]
|
let mut checkpoint = settled_checkpoint();
|
||||||
struct Checkpoint {
|
assert!(validate_settled_activation(&checkpoint_extension(&checkpoint)).is_ok());
|
||||||
next_activation_sequence: u64,
|
|
||||||
activating: Option<u8>,
|
|
||||||
activating_notification: Vec<u8>,
|
|
||||||
pending: Vec<u8>,
|
|
||||||
pending_notifications: Vec<u8>,
|
|
||||||
receipts: Vec<u8>,
|
|
||||||
notification_receipts: Vec<u8>,
|
|
||||||
invocation_receipts: Vec<u8>,
|
|
||||||
invocation_recovery_blocked: bool,
|
|
||||||
}
|
|
||||||
let extension = |checkpoint: &Checkpoint| {
|
|
||||||
session_store::SessionExtension::new(
|
|
||||||
"worker.pending_activations.v1",
|
|
||||||
serde_json::to_value(checkpoint).unwrap(),
|
|
||||||
)
|
|
||||||
};
|
|
||||||
let mut checkpoint = Checkpoint {
|
|
||||||
receipts: vec![1, 2],
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
assert!(validate_settled_activation(&extension(&checkpoint)).is_ok());
|
|
||||||
checkpoint.pending.push(1);
|
checkpoint.pending.push(1);
|
||||||
assert!(validate_settled_activation(&extension(&checkpoint)).is_err());
|
assert!(validate_settled_activation(&checkpoint_extension(&checkpoint)).is_err());
|
||||||
checkpoint.pending.clear();
|
checkpoint.pending.clear();
|
||||||
checkpoint.activating = Some(1);
|
checkpoint.activating = Some(1);
|
||||||
assert!(validate_settled_activation(&extension(&checkpoint)).is_err());
|
assert!(validate_settled_activation(&checkpoint_extension(&checkpoint)).is_err());
|
||||||
checkpoint.activating = None;
|
checkpoint.activating = None;
|
||||||
checkpoint.invocation_receipts.push(1);
|
checkpoint.invocation_receipts.push(1);
|
||||||
assert!(validate_settled_activation(&extension(&checkpoint)).is_err());
|
assert!(validate_settled_activation(&checkpoint_extension(&checkpoint)).is_err());
|
||||||
checkpoint.invocation_receipts.clear();
|
checkpoint.invocation_receipts.clear();
|
||||||
checkpoint.invocation_recovery_blocked = true;
|
checkpoint.invocation_recovery_blocked = true;
|
||||||
assert!(validate_settled_activation(&extension(&checkpoint)).is_err());
|
assert!(validate_settled_activation(&checkpoint_extension(&checkpoint)).is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deliberately corrupt only test-owned synthetic source files, never the lab.
|
||||||
|
fn assert_rejected_session_bytes(worker: &Path, template: &StoppedWorkerTemplate, bytes: Vec<u8>) {
|
||||||
|
let path = worker.join(format!("session/segments/{}.jsonl", template.segment_id()));
|
||||||
|
fs::write(path, &bytes).unwrap();
|
||||||
|
let extraction_error = StoppedWorkerTemplate::extract(worker)
|
||||||
|
.err()
|
||||||
|
.expect("invalid source accepted");
|
||||||
|
assert!(
|
||||||
|
!extraction_error
|
||||||
|
.to_string()
|
||||||
|
.contains("synthetic-discarded-material")
|
||||||
|
);
|
||||||
|
let (_target, options) = destination(template);
|
||||||
|
let mut invalid = template.clone();
|
||||||
|
invalid.segment_jsonl = bytes;
|
||||||
|
let installation_error = invalid
|
||||||
|
.install(&options, request(), &resolved())
|
||||||
|
.unwrap_err();
|
||||||
|
assert!(
|
||||||
|
!installation_error
|
||||||
|
.to_string()
|
||||||
|
.contains("synthetic-discarded-material")
|
||||||
|
);
|
||||||
|
assert_fresh(&options);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn synthetic_logs(template: &StoppedWorkerTemplate) -> Vec<LogEntry> {
|
||||||
|
template
|
||||||
|
.segment_jsonl
|
||||||
|
.split(|byte| *byte == b'\n')
|
||||||
|
.filter(|line| !line.is_empty())
|
||||||
|
.map(|line| serde_json::from_slice(line).unwrap())
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn encode_synthetic_logs(logs: &[LogEntry]) -> Vec<u8> {
|
||||||
|
let mut bytes = Vec::new();
|
||||||
|
for log in logs {
|
||||||
|
serde_json::to_writer(&mut bytes, log).unwrap();
|
||||||
|
bytes.push(b'\n');
|
||||||
|
}
|
||||||
|
bytes
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn missing_system_prompt_matches_worker_restore_rejection() {
|
||||||
|
let (_source, worker_dir, template) = source();
|
||||||
|
let mut logs = synthetic_logs(&template);
|
||||||
|
assert!(session_store::collect_state(&logs).system_prompt.is_some());
|
||||||
|
let LogEntry::AnnotatedSegmentStart { system_prompt, .. } = &mut logs[0] else {
|
||||||
|
panic!("missing seed")
|
||||||
|
};
|
||||||
|
*system_prompt = None;
|
||||||
|
assert_rejected_session_bytes(&worker_dir, &template, encode_synthetic_logs(&logs));
|
||||||
|
// The owning Worker constructor rejects this before model-client creation,
|
||||||
|
// allocation, or execution. Do not start a controller or submit any input.
|
||||||
|
let store = session_store::CombinedStore::new(
|
||||||
|
WorkerSessionStore::open_read_only(worker_dir.join("session")).unwrap(),
|
||||||
|
session_store::WorkerAggregateStore::open_read_only(&worker_dir, template.worker_name())
|
||||||
|
.unwrap(),
|
||||||
|
);
|
||||||
|
let restored = worker::Worker::restore_from_manifest(
|
||||||
|
template.session_id(),
|
||||||
|
template.segment_id(),
|
||||||
|
resolved(),
|
||||||
|
store,
|
||||||
|
worker::PromptCatalogSource::builtins_only(),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
assert!(matches!(
|
||||||
|
restored,
|
||||||
|
Err(worker::WorkerError::SegmentSystemPromptMissing { .. })
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn malformed_submission_and_notification_receipts_fail_extraction_and_install() {
|
||||||
|
let (_source, worker, template) = source();
|
||||||
|
for notification in [false, true] {
|
||||||
|
let mut checkpoint = settled_checkpoint();
|
||||||
|
if notification {
|
||||||
|
checkpoint.notification_receipts =
|
||||||
|
vec![ReceiptFixture::Malformed(1), ReceiptFixture::Malformed(2)];
|
||||||
|
} else {
|
||||||
|
checkpoint.receipts = vec![ReceiptFixture::Malformed(1), ReceiptFixture::Malformed(2)];
|
||||||
|
}
|
||||||
|
let mut logs = synthetic_logs(&template);
|
||||||
|
for log in &mut logs {
|
||||||
|
if let LogEntry::AnnotatedUserInput { extensions, .. } = log {
|
||||||
|
*extensions = vec![checkpoint_extension(&checkpoint)];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert_rejected_session_bytes(&worker, &template, encode_synthetic_logs(&logs));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn unknown_fields_at_every_retained_record_depth_are_rejected_without_secret_diagnostics() {
|
||||||
|
let (_source, worker, template) = source();
|
||||||
|
let original = String::from_utf8(template.segment_jsonl.clone()).unwrap();
|
||||||
|
// These values are harmless synthetic markers, not lab/private material.
|
||||||
|
// The compatibility decoder normally discards many of these fields.
|
||||||
|
let locations = [
|
||||||
|
r#""kind":"annotated_segment_start""#,
|
||||||
|
r#""kind":"invoke""#,
|
||||||
|
r#""kind":"annotated_user_input""#,
|
||||||
|
r#""kind":"annotated_assistant_item""#,
|
||||||
|
r#""kind":"llm_usage""#,
|
||||||
|
r#""kind":"turn_end""#,
|
||||||
|
r#""kind":"run_completed""#,
|
||||||
|
r#""max_tokens":null"#,
|
||||||
|
r#""kind":"message","role":"assistant""#,
|
||||||
|
r#""kind":"text","text":"artificial fixture reply""#,
|
||||||
|
r#""kind":"text","content":"Harmless artificial fixture input.""#,
|
||||||
|
r#""entry_id":"fixed-entry-id""#,
|
||||||
|
r#""kind":"model_output""#,
|
||||||
|
r#""worker_id":"01a1657d-b762-7791-87ae-a49f28cadbce""#,
|
||||||
|
r#""domain":"worker.pending_activations.v1""#,
|
||||||
|
r#""next_activation_sequence":2"#,
|
||||||
|
r#""submission_request_id":"completed-submission-request""#,
|
||||||
|
r#""notification_request_id":"completed-notification-request""#,
|
||||||
|
];
|
||||||
|
for location in locations {
|
||||||
|
assert!(original.contains(location));
|
||||||
|
for extra in [
|
||||||
|
r#", "reasoning":{"text":"synthetic-discarded-material"}"#,
|
||||||
|
r#", "encrypted_content":"synthetic-discarded-material""#,
|
||||||
|
] {
|
||||||
|
let altered = original.replacen(location, &format!("{location}{extra}"), 1);
|
||||||
|
assert_rejected_session_bytes(&worker, &template, altered.into_bytes());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let duplicate = original.replacen(
|
||||||
|
r#""role":"assistant""#,
|
||||||
|
r#""role":"assistant","role":"assistant""#,
|
||||||
|
1,
|
||||||
|
);
|
||||||
|
assert_rejected_session_bytes(&worker, &template, duplicate.into_bytes());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn opaque_feature_payloads_cannot_bypass_strict_session_validation() {
|
||||||
|
let (_source, worker, template) = source();
|
||||||
|
let mut logs = synthetic_logs(&template);
|
||||||
|
for log in &mut logs {
|
||||||
|
if let LogEntry::AnnotatedUserInput { history, .. } = log {
|
||||||
|
if let session_store::LoggedItem::Message { role, .. } = &mut history[0].item {
|
||||||
|
*role = session_store::LoggedRole::System;
|
||||||
|
}
|
||||||
|
history[0].metadata.input_metadata = Some(
|
||||||
|
session_store::HistoryInputMetadata::new(
|
||||||
|
session_store::HistoryInputUsePolicy {
|
||||||
|
recording_material: session_store::InputEvidenceUse::Exclude,
|
||||||
|
memory_evidence: session_store::InputEvidenceUse::Exclude,
|
||||||
|
observation: session_store::InputObservationUse::Hidden,
|
||||||
|
compaction: session_store::InputCompactionUse::Retain,
|
||||||
|
},
|
||||||
|
Some(
|
||||||
|
session_store::FeatureInputPayload::encode(
|
||||||
|
"fixture.test",
|
||||||
|
1,
|
||||||
|
&BTreeMap::from([("reasoning", "harmless synthetic payload")]),
|
||||||
|
)
|
||||||
|
.unwrap(),
|
||||||
|
),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.unwrap(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert_rejected_session_bytes(&worker, &template, encode_synthetic_logs(&logs));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
|
|||||||
Reference in New Issue
Block a user