review: memory writes from a secret session are not written to Honcho
The journal now records each call's class (absent on the owner's undo and on older lines, which still read). The mirror turns everything a secret write would conclude or send to the fleet into one Skip, after its deletions, which still go; a note whose own header says secret is skipped the same way. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -4,7 +4,7 @@
|
||||
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use bxcore::{SessionId, Timestamp};
|
||||
use bxcore::{DataClass, SessionId, Timestamp};
|
||||
|
||||
use crate::files::Store;
|
||||
use crate::honcho::ledger::{self, Action, Item, Ledger, Record, Workspace};
|
||||
@@ -18,6 +18,9 @@ pub const OWNER: &str = "owner";
|
||||
/// The part of an edit that has no Boxmaker session to stamp its conclusion with.
|
||||
pub const NO_SESSION: &str = "the edit has no Boxmaker session";
|
||||
|
||||
/// Why a write from a `secret` session sends nothing but its deletions (M5c spec, section 3).
|
||||
pub const SECRET_WRITE: &str = "the write is from a secret session";
|
||||
|
||||
/// `before` and `after` as multisets of entry text: what `before` has more of (dropped, in
|
||||
/// `before`'s order), and what `after` has more of (added, in `after`'s order).
|
||||
pub fn diff(before: &[String], after: &[String]) -> (Vec<String>, Vec<String>) {
|
||||
@@ -138,8 +141,25 @@ fn note_actions(
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Whether `record` came from a `secret` session: by the class the journal gives the call, or, for
|
||||
/// a line from before it did, by the class any of its notes was written with.
|
||||
fn is_secret(record: &journal::Record, notes: &[notes::Note]) -> bool {
|
||||
record.class.as_deref() == Some(notes::class_name(DataClass::Secret))
|
||||
|| notes.iter().any(|n| {
|
||||
matches!(
|
||||
n.kind,
|
||||
Kind::Noted {
|
||||
class: DataClass::Secret,
|
||||
..
|
||||
}
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/// What `record` asks of Honcho, in the order to send it: an identity edit's, or a write saved as
|
||||
/// notes. Anything else (a rejection) asks nothing.
|
||||
/// notes. Anything else (a rejection) asks nothing. From a `secret` session only the deletions
|
||||
/// go, in their order, and everything else the write asked is one `Skip` after them: deleting
|
||||
/// leaks nothing and keeps Honcho consistent with the files.
|
||||
pub fn actions(
|
||||
record: &journal::Record,
|
||||
session: Option<&SessionId>,
|
||||
@@ -147,10 +167,22 @@ pub fn actions(
|
||||
owner: &str,
|
||||
) -> Vec<Action> {
|
||||
let notes = notes_of(record);
|
||||
match &record.file {
|
||||
let all = match &record.file {
|
||||
Some(file) => identity_actions(record, file, ¬es, session, agent, owner),
|
||||
None => note_actions(record, ¬es, session, agent),
|
||||
};
|
||||
if !is_secret(record, ¬es) {
|
||||
return all;
|
||||
}
|
||||
let (mut kept, held): (Vec<Action>, Vec<Action>) = all
|
||||
.into_iter()
|
||||
.partition(|a| matches!(a, Action::Unconclude { .. }));
|
||||
if !held.is_empty() {
|
||||
kept.push(Action::Skip {
|
||||
why: SECRET_WRITE.to_string(),
|
||||
});
|
||||
}
|
||||
kept
|
||||
}
|
||||
|
||||
/// With `[honcho]`: decides each journalled edit into the ledger.
|
||||
|
||||
@@ -35,6 +35,10 @@ pub struct Record {
|
||||
/// For an undo, the edit it reverted.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub undoes: Option<String>,
|
||||
/// The call's class (`public`, `private` or `secret`, as on the wire): what its session had
|
||||
/// read. `None` for the owner's undo and for lines from before it was recorded.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub class: Option<String>,
|
||||
}
|
||||
|
||||
fn file_of(name: &str) -> Option<MemoryFile> {
|
||||
@@ -132,6 +136,7 @@ pub fn record(store: &Store, ctx: &Context, outcome: &Outcome) -> Result<String,
|
||||
untrusted: ctx.untrusted,
|
||||
flag: outcome.flag.map(str::to_string),
|
||||
undoes: None,
|
||||
class: Some(crate::notes::class_name(ctx.taint).to_string()),
|
||||
},
|
||||
ctx.now,
|
||||
)
|
||||
@@ -227,6 +232,7 @@ pub fn undo(store: &Store, id: &str, now: Timestamp) -> Result<String, String> {
|
||||
untrusted: false,
|
||||
flag: None,
|
||||
undoes: Some(id.to_string()),
|
||||
class: None,
|
||||
},
|
||||
now,
|
||||
)?;
|
||||
|
||||
@@ -37,7 +37,8 @@ pub fn minute(time: Timestamp) -> String {
|
||||
format!("{}Z", s.get(..16).unwrap_or(&s))
|
||||
}
|
||||
|
||||
fn class_name(class: DataClass) -> &'static str {
|
||||
/// A class as the notes and the journal spell it, the same as on the wire.
|
||||
pub fn class_name(class: DataClass) -> &'static str {
|
||||
match class {
|
||||
DataClass::Public => "public",
|
||||
DataClass::Private => "private",
|
||||
|
||||
@@ -246,6 +246,7 @@ pub fn reject(store: &Store, id: &str, now: Timestamp) -> Result<String, String>
|
||||
untrusted: false,
|
||||
flag: None,
|
||||
undoes: Some(id.to_string()),
|
||||
class: None,
|
||||
},
|
||||
now,
|
||||
)?;
|
||||
|
||||
Reference in New Issue
Block a user