Watch
1
0
Fork
You've already forked souveraine
0

session trust: step-up send gating + lock-time response redaction

Step-up send gating:
- Souveraine.send() returns 'step-up' when auth is required
- Ai.sendUserMessage() triggers StepUpAuth.requestAuth('send') on
  step-up, retries on success, shows message on failure
- Extracted _startStreaming() helper for reuse after auth retry

Lock-time response redaction:
- Ai.qml watches GlobalStates.screenLocked
- On lock mid-stream: replaces displayed content with '[content hidden
  until unlock]', preserves rawContent for post-unlock display
- Enforces SESSION-TRUST-ARCHITECTURE.md requirement: lock during
  personal agent output hides it

Trust boundary matrix updated.
This commit is contained in:
Fimeg 2026-07-14 21:13:50 -04:00
commit 61bdc5360f
3 changed files with 69 additions and 4 deletions

View file

@ -35,6 +35,7 @@ is not — if an operation is not in this matrix, it is not gated.
| Agent conversation | Souveraine IPC | ambient | Server auth token | enforced |
| Agent send with personal context | Souveraine IPC | personal | !screenLocked && !screenLockSecure | enforced via LockContentPolicy |
| Agent output on lock surface | Lock surface | ambient | LockContentPolicy.allowsOnLock() | enforced |
| In-flight agent response on lock | Lock surface | personal | Ai.qml redacts on screenLocked | enforced |
| Media metadata on lock surface | Lock surface | ambient (if opted) | LockContentPolicy.mediaMetadataAmbient | enforced |
| Media transport controls | Lock surface | ambient | LockContentPolicy.mediaControlsVisible | enforced |
| Lock screen power actions | Lock surface | ambient | Config.lock.security.requirePasswordToPower | enforced |
@ -48,7 +49,7 @@ is not — if an operation is not in this matrix, it is not gated.
| Agent delete/push operations | Souveraine IPC | stepUp | StepUpAuth built but minTier metadata not wired |
| Agent physical access | Souveraine IPC | stepUp | StepUpAuth built but minTier metadata not wired |
| Break-glass override | Emergency | scoped grant | Not implemented |
| Boot-time IPC audit | Shell startup | ambient | Not implemented |
| Boot-time IPC audit | Shell startup | ambient | Done (log in Session.qml) |
## Notes

View file

@ -119,6 +119,31 @@ Singleton {
}
}
// Lock-time response redaction
// When the session locks during a personal-tier streaming response,
// the in-flight content must be withheld immediately. Only ambient
// output remains visible on the lock surface. This is the enforcement
// side of SESSION-TRUST-ARCHITECTURE.md's "A lock during personal
// agent output hides it" requirement.
//
// The streaming message is replaced with a redaction placeholder. The
// raw content is preserved in the message's rawContent so it can be
// shown again after unlock (the message stays in history), but the
// displayed content is cleared.
Connections {
target: GlobalStates
function onScreenLockedChanged() {
if (!GlobalStates.screenLocked) return;
if (!root.streamingMessage) return;
// Lock fired mid-stream. Redact the displayed content.
// rawContent preserves the actual response for post-unlock.
if (root.streamingMessage.content.length > 0) {
root.streamingMessage.content = Translation.tr("[content hidden until unlock]");
console.log("[ai] lock fired during stream — redacted personal output");
}
}
}
// Streaming message shaping
property AiMessageData streamingMessage
property bool inThinkBlock: false
@ -306,11 +331,37 @@ Singleton {
function sendUserMessage(message) {
if (message.length === 0) return;
root.addMessage(message, "user");
if (!Souveraine.send(message)) {
const result = Souveraine.send(message);
if (result === "step-up") {
// Step-up auth required. Trigger the PAM flow; on success,
// retry the send. The user message is already in the chat
// history, so we don't add it again.
if (typeof StepUpAuth !== "undefined") {
StepUpAuth.requestAuth("send", function(granted) {
if (granted) {
// Remove the "auth required" indicator if one was
// added, and retry. The queued text was not consumed
// by Souveraine, so we can send it again.
Souveraine.send(message);
// Re-create the streaming message for the response.
root._startStreaming();
} else {
root.addMessage(Translation.tr("Authentication required to send."), root.interfaceRole);
}
});
}
return;
}
if (!result) {
root.addMessage(Translation.tr("Souveraine server unreachable at %1 — start it with `souveraine server`").arg(Souveraine.serverBase), root.interfaceRole);
return;
}
/* Streaming assistant message; filled by handleStreamEvent */
root._startStreaming();
}
// Set up the streaming assistant message container. Called after a
// successful send (or after a step-up auth retry succeeds).
function _startStreaming() {
root.inThinkBlock = false;
root.streamingMessage = root.aiMessageComponent.createObject(root, {
"role": "assistant",

View file

@ -56,6 +56,9 @@ Singleton {
// Emitted after a server-owned conversation has been selected and its
// persisted transcript loaded for the active surface.
signal conversationResumed(string agentId, string conversationId, var messages)
// Emitted when a send is blocked by step-up auth. The UI should call
// StepUpAuth.requestAuth("send", callback) and retry on success.
signal stepUpRequired(string actionFamily, string queuedText)
// Agent inventory
Process {
@ -274,10 +277,20 @@ Singleton {
property string _queuedText: ""
/* Send a user message with ambient context. Returns false if the
server is down or no agent is selected. */
server is down or no agent is selected. Returns "step-up" if
step-up auth is required but no valid grant exists the caller
should trigger StepUpAuth.requestAuth("send") and retry. */
function send(text) {
if (!root.serverUp || root.currentAgentId.length === 0) return false;
if (text.length === 0) return false;
// Step-up gate: if enabled and no valid send grant exists, block
// the send and emit a signal so the UI can trigger auth + retry.
if (Config.options?.lock?.stepUp?.enabled
&& typeof StepUpAuth !== "undefined"
&& !StepUpAuth.isGranted("send")) {
root.stepUpRequired("send", text);
return "step-up";
}
root._queuedText = text;
if (root.ambientEnabled) {
cursorProc.running = true; // chain continues in onExited