Watch
1
0
Fork
You've already forked souveraine
0

lock surface: restore redacted chat on unlock + announce replies

Redacted messages stayed '[content hidden until unlock]' forever — nothing
restored them on unlock. Redaction now records the message and unlock
restores content from rawContent. Turns finishing while locked are also
redacted and surfaced. New LockAgentCard shows a one-line preview of
replies that landed during the lock window on the lock glance, like a
notification; body stays hidden until unlock.

sessiond auth: getpwuid_r buffer is u8 with a c_char cast so it compiles
on aarch64 (c_char = u8 there) and x86 (c_char = i8).
This commit is contained in:
Fimeg 2026-07-16 21:16:28 -04:00
commit e10efbbd56
6 changed files with 160 additions and 9 deletions

View file

@ -158,9 +158,18 @@ pub fn session_user() -> Result<String, String> {
unsafe { unsafe {
let uid = libc::getuid(); let uid = libc::getuid();
let mut pwd: libc::passwd = std::mem::zeroed(); let mut pwd: libc::passwd = std::mem::zeroed();
let mut buf = [0i8; 1024]; let mut buf = [0u8; 1024];
let mut result: *mut libc::passwd = std::ptr::null_mut(); let mut result: *mut libc::passwd = std::ptr::null_mut();
let rc = libc::getpwuid_r(uid, &mut pwd, buf.as_mut_ptr(), buf.len(), &mut result); // libc's `c_char` is i8 on some targets and u8 on others (aarch64),
// so the buffer is `u8` and we cast through `c_char` to satisfy
// whichever signature the target's libc crate exposes.
let rc = libc::getpwuid_r(
uid,
&mut pwd,
buf.as_mut_ptr() as *mut libc::c_char,
buf.len(),
&mut result,
);
if rc != 0 || result.is_null() { if rc != 0 || result.is_null() {
return Err(format!("getpwuid_r failed for uid {uid}")); return Err(format!("getpwuid_r failed for uid {uid}"));
} }

View file

@ -73,6 +73,7 @@ modules/common/panels/lock/LockScreen.qml souveraine/modules/common/panels/lock/
modules/ii/lock/Lock.qml souveraine/modules/ii/lock/Lock.qml modules/ii/lock/Lock.qml souveraine/modules/ii/lock/Lock.qml
modules/ii/lock/TouchLockSurface.qml souveraine/modules/ii/lock/TouchLockSurface.qml modules/ii/lock/TouchLockSurface.qml souveraine/modules/ii/lock/TouchLockSurface.qml
modules/souveraine/lock/LockMediaCard.qml souveraine/modules/souveraine/lock/LockMediaCard.qml modules/souveraine/lock/LockMediaCard.qml souveraine/modules/souveraine/lock/LockMediaCard.qml
modules/souveraine/lock/LockAgentCard.qml souveraine/modules/souveraine/lock/LockAgentCard.qml
modules/souveraine/lock/LockSurfaceHost.qml souveraine/modules/souveraine/lock/LockSurfaceHost.qml modules/souveraine/lock/LockSurfaceHost.qml souveraine/modules/souveraine/lock/LockSurfaceHost.qml
modules/souveraine/lock/qmldir souveraine/modules/souveraine/lock/qmldir modules/souveraine/lock/qmldir souveraine/modules/souveraine/lock/qmldir
modules/souveraine/navigation/SystemGestureRail.qml souveraine/modules/souveraine/navigation/SystemGestureRail.qml modules/souveraine/navigation/SystemGestureRail.qml souveraine/modules/souveraine/navigation/SystemGestureRail.qml

View file

@ -0,0 +1,77 @@
// Ambient "the agent replied" card. While the session is locked, personal-tier
// agent output is redacted in the chat (Ai.qml). This surface announces that a
// reply landed one-line preview only, never the body so the lock screen
// behaves like a notification: you see that she answered, and the full text
// restores in the chat on unlock. See SESSION-TRUST-ARCHITECTURE.md (ambient
// output is safe on the lock surface; personal is not).
import QtQuick
import QtQuick.Layouts
import qs.services
Item {
id: root
property var previews: []
implicitHeight: card.visible ? card.implicitHeight : 0
visible: previews.length > 0
function refresh() {
root.previews = Ai.redactedPreviews();
}
Connections {
target: Ai
function onRedactedMessagesChanged() { root.refresh() }
}
Connections {
target: GlobalStates
// Repopulate on lock, clear on unlock (the chat takes over).
function onScreenLockedChanged() {
if (GlobalStates.screenLocked) root.refresh();
else root.previews = [];
}
}
Rectangle {
id: card
anchors.fill: parent
visible: root.previews.length > 0
radius: 16
color: "#22000000"
border.color: "#33ffffff"
border.width: 1
implicitHeight: layout.implicitHeight + 24
ColumnLayout {
id: layout
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
anchors.margins: 12
spacing: 4
Text {
Layout.fillWidth: true
text: qsTr("Souveraine replied while locked")
color: "#ccffffff"
font.pixelSize: 12
font.weight: Font.DemiBold
elide: Text.ElideRight
}
Repeater {
model: root.previews.slice(0, 3)
delegate: Text {
required property string modelData
Layout.fillWidth: true
text: "• " + modelData
color: "#e6ffffff"
font.pixelSize: 14
elide: Text.ElideRight
maximumLineCount: 1
}
}
}
}
}

View file

@ -65,5 +65,11 @@ Item {
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
Layout.fillWidth: true Layout.fillWidth: true
} }
LockAgentCard {
Layout.topMargin: root.compact ? 6 : 10
Layout.alignment: Qt.AlignHCenter
Layout.fillWidth: true
}
} }
} }

View file

@ -1,3 +1,4 @@
module qs.modules.souveraine.lock module qs.modules.souveraine.lock
LockMediaCard 1.0 LockMediaCard.qml LockMediaCard 1.0 LockMediaCard.qml
LockSurfaceHost 1.0 LockSurfaceHost.qml LockSurfaceHost 1.0 LockSurfaceHost.qml
LockAgentCard 1.0 LockAgentCard.qml

View file

@ -131,20 +131,71 @@ Singleton {
// raw content is preserved in the message's rawContent so it can be // raw content is preserved in the message's rawContent so it can be
// shown again after unlock (the message stays in history), but the // shown again after unlock (the message stays in history), but the
// displayed content is cleared. // displayed content is cleared.
//
// The redaction is reversible: every message whose content we replaced
// with the placeholder is recorded in `redactedOnLock`, and on unlock
// its content is restored from `rawContent` (the streaming message too,
// if it is still in flight). A finished message redacted at lock then
// surfaced on the lock screen counts as "delivered" and is restored
// normally on unlock; one that never surfaced stays for the chat to
// replay once unlocked.
property var redactedOnLock: []
function _redactMessage(msg) {
if (!msg || msg.content.length === 0) return;
if (msg.content === Translation.tr("[content hidden until unlock]")) return;
msg.rawContent = msg.content;
msg.content = Translation.tr("[content hidden until unlock]");
if (!root.redactedOnLock.includes(msg)) {
root.redactedOnLock = [...root.redactedOnLock, msg];
}
root.redactedMessagesChanged();
}
function _restoreRedacted() {
if (root.redactedOnLock.length === 0) return;
for (const msg of root.redactedOnLock) {
if (msg && msg.rawContent && msg.rawContent.length > 0) {
msg.content = msg.rawContent;
}
}
root.redactedOnLock = [];
root.redactedMessagesChanged();
}
// Messages redacted while locked, newest first, for the lock surface to
// surface as ambient notifications. Strips to a one-line preview the
// lock screen shows "the agent replied", not the personal-tier body.
function redactedPreviews() {
return root.redactedOnLock
.filter(m => m && m.rawContent && m.rawContent.length > 0 && m.role === "assistant")
.map(m => {
const firstLine = m.rawContent.split("\n").find(l => l.trim().length > 0) || "";
return firstLine.slice(0, 80);
})
.reverse();
}
signal redactedMessagesChanged()
Connections { Connections {
target: GlobalStates target: GlobalStates
function onScreenLockedChanged() { function onScreenLockedChanged() {
if (!GlobalStates.screenLocked) return; if (GlobalStates.screenLocked) {
if (!root.streamingMessage) return; // Lock fired mid-stream redact the in-flight content.
// Lock fired mid-stream. Redact the displayed content. if (root.streamingMessage) {
// rawContent preserves the actual response for post-unlock. root._redactMessage(root.streamingMessage);
if (root.streamingMessage.content.length > 0) { console.log("[ai] lock fired during stream — redacted personal output");
root.streamingMessage.content = Translation.tr("[content hidden until unlock]"); }
console.log("[ai] lock fired during stream — redacted personal output"); } else {
// Unlock: restore every message we hid, so the chat shows
// what actually came back rather than lingering placeholders.
root._restoreRedacted();
} }
} }
} }
// Streaming message shaping // Streaming message shaping
property AiMessageData streamingMessage property AiMessageData streamingMessage
property bool inThinkBlock: false property bool inThinkBlock: false
@ -171,6 +222,12 @@ Singleton {
} }
root.streamingMessage.thinking = false; root.streamingMessage.thinking = false;
root.streamingMessage.done = true; root.streamingMessage.done = true;
// If the turn finished while the session is locked, the finished
// assistant message is personal-tier output the user hasn't seen
// redact it for the chat view and let the lock surface announce it.
if (GlobalStates.screenLocked && root.streamingMessage.content.length > 0) {
root._redactMessage(root.streamingMessage);
}
if (root.postResponseHook) { if (root.postResponseHook) {
root.postResponseHook(); root.postResponseHook();
root.postResponseHook = null; root.postResponseHook = null;