auth: put mail ingestion on the same rung as resolving a task

IngestMail was AuthRead and SetTaskStatus was AuthWrite, and they answer
the same question: may this module change what is on his lists? The old
argument for AuthRead — ingestion is additive, it can only produce
candidate tasks — is still true and is the weaker half, because a
compromised mail reader that can fill the review page indefinitely is
not a read.

Nothing loses access. AuthWrite outside WriteFact only requires
enrollment, which mavmaild already has, and the method does not exist
unless the operator wired a mail block.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TrVSBKe3RFDF4fGYKWYQnX
This commit is contained in:
kami
2026-08-01 16:29:45 +04:00
parent 76a251a20d
commit df3220d039
2 changed files with 36 additions and 5 deletions
+22
View File
@@ -417,6 +417,28 @@ func TestRequirement_SwapModel(t *testing.T) {
}
}
// TestRequirement_ListMutation — the three methods that change what is on his
// lists sit on one rung. IngestMail joined them on 2026-08-01; it used to be
// AuthRead, which made it disagree with SetTaskStatus about the same question.
// CaptureTask stays a read: it puts one line on a list he asked for.
func TestRequirement_ListMutation(t *testing.T) {
for _, m := range []ipc.Method{
ipc.MethodIngestMail, ipc.MethodSetTaskStatus,
} {
if got := Requirement(m); got != AuthWrite {
t.Errorf("%s authority = %v; want AuthWrite", m, got)
}
}
if got := Requirement(ipc.MethodCaptureTask); got != AuthRead {
t.Errorf("CaptureTask authority = %v; want AuthRead", got)
}
// mavmaild keeps working: AuthWrite outside WriteFact only needs enrollment.
maild := Scope{Surface: SurfaceCoreProcess, Module: "mavmaild", SourceScope: []string{"mail:*"}}
if err := Can(ipc.MethodIngestMail, maild, nil); err != nil {
t.Errorf("mavmaild ingesting mail = %v; want allowed", err)
}
}
// TestRequirement_Capture — recording other people is a write, not a read: it
// puts audio of them on disk. The read side, "что ты записываешь?", is not.
//
+14 -5
View File
@@ -93,6 +93,20 @@ func Requirement(m ipc.Method) Authority {
return AuthWrite
case ipc.MethodWriteFact:
return AuthWrite
case ipc.MethodIngestMail:
// Mail ingestion (Vikunja #246). Moved up from AuthRead on 2026-08-01,
// for consistency with SetTaskStatus rather than for a new threat: both
// answer the same question — may this module change what is on his
// lists? — and they were answering it differently. The old argument was
// that ingestion is additive and can only produce candidate tasks, which
// is still true; it is the weaker half of the argument, because a
// compromised mail reader that can fill the review page indefinitely is
// not a read.
//
// No caller loses anything: AuthWrite outside WriteFact only requires
// enrollment, which mavmaild already has, and the method does not exist
// unless the operator wired a mail block.
return AuthWrite
case ipc.MethodSetTaskStatus:
// Resolving a task is NOT additive, which is what separates it from
// capture. Capture at AuthRead can only put a line on a list he reads
@@ -122,11 +136,6 @@ func Requirement(m ipc.Method) Authority {
// something.
ipc.MethodCaptureTask,
ipc.MethodListTasks,
// Mail ingestion (Vikunja #246). AuthRead because of what the method can
// produce: candidate tasks and nothing else. It cannot write a fact, set a
// reminder, or touch the tool allowlist, so a compromised mail reader can
// at worst put junk on a review page he clears in one click.
ipc.MethodIngestMail,
// Looking at one image (Vikunja #252). AuthRead because of what it can
// produce: words about a picture, and optionally a note. It cannot write
// a fact, set a reminder, or touch the tool allowlist. The invasive part