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 conversation | Souveraine IPC | ambient | Server auth token | enforced |
|
||||||
| Agent send with personal context | Souveraine IPC | personal | !screenLocked && !screenLockSecure | enforced via LockContentPolicy |
|
| Agent send with personal context | Souveraine IPC | personal | !screenLocked && !screenLockSecure | enforced via LockContentPolicy |
|
||||||
| Agent output on lock surface | Lock surface | ambient | LockContentPolicy.allowsOnLock() | enforced |
|
| 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 metadata on lock surface | Lock surface | ambient (if opted) | LockContentPolicy.mediaMetadataAmbient | enforced |
|
||||||
| Media transport controls | Lock surface | ambient | LockContentPolicy.mediaControlsVisible | enforced |
|
| Media transport controls | Lock surface | ambient | LockContentPolicy.mediaControlsVisible | enforced |
|
||||||
| Lock screen power actions | Lock surface | ambient | Config.lock.security.requirePasswordToPower | 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 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 |
|
| Agent physical access | Souveraine IPC | stepUp | StepUpAuth built but minTier metadata not wired |
|
||||||
| Break-glass override | Emergency | scoped grant | Not implemented |
|
| 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
|
## 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 ────────────────────────────────────────
|
// ── Streaming message shaping ────────────────────────────────────────
|
||||||
property AiMessageData streamingMessage
|
property AiMessageData streamingMessage
|
||||||
property bool inThinkBlock: false
|
property bool inThinkBlock: false
|
||||||
|
|
@ -306,11 +331,37 @@ Singleton {
|
||||||
function sendUserMessage(message) {
|
function sendUserMessage(message) {
|
||||||
if (message.length === 0) return;
|
if (message.length === 0) return;
|
||||||
root.addMessage(message, "user");
|
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);
|
root.addMessage(Translation.tr("Souveraine server unreachable at %1 — start it with `souveraine server`").arg(Souveraine.serverBase), root.interfaceRole);
|
||||||
return;
|
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.inThinkBlock = false;
|
||||||
root.streamingMessage = root.aiMessageComponent.createObject(root, {
|
root.streamingMessage = root.aiMessageComponent.createObject(root, {
|
||||||
"role": "assistant",
|
"role": "assistant",
|
||||||
|
|
|
||||||
|
|
@ -56,6 +56,9 @@ Singleton {
|
||||||
// Emitted after a server-owned conversation has been selected and its
|
// Emitted after a server-owned conversation has been selected and its
|
||||||
// persisted transcript loaded for the active surface.
|
// persisted transcript loaded for the active surface.
|
||||||
signal conversationResumed(string agentId, string conversationId, var messages)
|
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 ──────────────────────────────────────────────────
|
// ── Agent inventory ──────────────────────────────────────────────────
|
||||||
Process {
|
Process {
|
||||||
|
|
@ -274,10 +277,20 @@ Singleton {
|
||||||
property string _queuedText: ""
|
property string _queuedText: ""
|
||||||
|
|
||||||
/* Send a user message with ambient context. Returns false if the
|
/* 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) {
|
function send(text) {
|
||||||
if (!root.serverUp || root.currentAgentId.length === 0) return false;
|
if (!root.serverUp || root.currentAgentId.length === 0) return false;
|
||||||
if (text.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;
|
root._queuedText = text;
|
||||||
if (root.ambientEnabled) {
|
if (root.ambientEnabled) {
|
||||||
cursorProc.running = true; // chain continues in onExited
|
cursorProc.running = true; // chain continues in onExited
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue