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:
parent
a8fd510dc7
commit
61bdc5360f
3 changed files with 69 additions and 4 deletions
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue