The CLI's --author names the session, it doesn't change the document

`--author` dispatched `file.setAuthor`, which a protected document refuses
now that the name is checked as the identity its exceptions are matched
against — so a script that named itself on a protected file failed before it
ran a single command.

The flag names the session, not the open document, so it sets the name
directly, with the same normalisation and blank-name check the command uses.
`file.setAuthor` keeps its protection check for the callers that mean to
change the document's identity.

Co-Authored-By: opencode <noreply@opencode.ai>
This commit is contained in:
Mat Strange
2026-10-20 17:20:53 +01:00
co-authored by opencode
parent f36339ab4b
commit d809c838c4
2 changed files with 26 additions and 9 deletions
+12 -1
View File
@@ -180,8 +180,12 @@ fn listed_text(doc: &wordcraft_doc::Document) -> Result<String, String> {
}
/// `--author NAME`: the author recorded on tracked changes and comments (`file.setAuthor`).
/// The name is set on the session directly: the caller is naming itself, not changing the open
/// document, so the protection that refuses `file.setAuthor` doesn't apply here.
fn set_author(s: &mut Session, name: &str) -> Result<(), String> {
s.run("file.setAuthor", &json!({"name": name})).map(|_| ()).map_err(|e| format!("--author: {e}"))
let name = wordcraft_engine::cmd::file::normalize_author(name).map_err(|e| format!("--author: {e}"))?;
s.author = name;
Ok(())
}
fn arg_value(args: &[String], name: &str) -> Option<String> {
@@ -804,6 +808,13 @@ mod tests {
assert!(run(&args(&["run", "--author", "Claude", "--cmd", "review.trackChanges={\"value\":true}"])).is_ok());
// The app's user name is the person's own; a bridge flag must not rewrite it.
assert!(run(&args(&["mcp", "--connect", "127.0.0.1:9", "--author", "Claude"])).is_err_and(|e| e.to_string().contains("--author")));
// The flag names the session, so a protected document doesn't refuse it — changing the
// document's identity is a different thing, and that is still refused.
let mut s = Session::new(wordcraft_doc::Document::new());
s.run("review.restrict", &json!({"mode": "readOnly"})).unwrap();
assert!(set_author(&mut s, "Claude").is_ok());
assert_eq!(s.author, "Claude");
assert!(s.run("file.setAuthor", &json!({"name": "Claude"})).is_err());
}
#[test]
+14 -8
View File
@@ -14,18 +14,24 @@ pub const FORMAT_FLAT_OPC: &str = "flatopc";
pub const MAX_AUTHOR_CHARS: usize = 255;
/// Sets the user name that new tracked changes and comments are recorded under. The name is
/// trimmed, stripped of control characters (they can't go into the saved XML) and cut to
/// [`MAX_AUTHOR_CHARS`]; a blank name is refused so revisions always carry an author.
/// A protected document refuses it: the name is the identity its exceptions are checked against.
pub(crate) fn set_author(s: &mut Session, v: &Value) -> CmdResult {
super::protect::check_user_name(s)?;
let name: String = p::req_str(v, "name")?.trim().chars().filter(|c| !c.is_control()).take(MAX_AUTHOR_CHARS).collect();
/// The user name as it is stored: trimmed, stripped of control characters (they can't go into
/// the saved XML) and cut to [`MAX_AUTHOR_CHARS`]; a blank name is refused so revisions always
/// carry an author.
pub fn normalize_author(name: &str) -> Result<String, CmdError> {
let name: String = name.trim().chars().filter(|c| !c.is_control()).take(MAX_AUTHOR_CHARS).collect();
let name = name.trim_end();
if name.is_empty() {
return Err(CmdError::Params("`name` must not be blank".into()));
}
s.author = name.to_string();
Ok(name.to_string())
}
/// Sets the user name that new tracked changes and comments are recorded under. The name is
/// normalised by [`normalize_author`].
/// A protected document refuses it: the name is the identity its exceptions are checked against.
pub(crate) fn set_author(s: &mut Session, v: &Value) -> CmdResult {
super::protect::check_user_name(s)?;
s.author = normalize_author(p::req_str(v, "name")?)?;
let mut r = sel_result(s)?;
if let Some(o) = r.as_object_mut() {
o.insert("name".into(), json!(s.author));