Skip to content

Commit c4373a6

Browse files
author
Neo
committed
Morpheus M-GUARDIAN piece 2: pre-transaction warning/recommend surface (advisory, no new gate)
GuardianReviewPanel shows piece 1's advisory observations on SendView's review screen, BEFORE the hard gate. Read-only display: - The Confirm & Send button is byte-for-byte unchanged — action Task { await sendNative() }, enabled state .disabled(isSending) ONLY; it does NOT depend on the observations. No acknowledge-to-proceed, no observation-based disable. The panel exposes no callback/binding the host must satisfy, so it structurally cannot influence whether the send proceeds. - sendNative's biometric -> server-preflight -> broadcast -> valid-hash sequence is byte-for-byte unchanged. guardianObservations is used only for @State, a read-only compute on entering review, and the display panel — it feeds no guard/branch/send decision. - Honest under-firing: todayOutgoingUSD passed as 0 (daily-soft under-fires the cumulative case, never over-claims); amountUSD nil with no price (no fabricated threshold). The panel footer makes the advisory nature unmistakable ("This is advice, not a block — you decide..."). Adversarial pass: 13 raised, 0 confirmed. Flags OFF, OBSERVE, testnet.
1 parent 0a8d527 commit c4373a6

3 files changed

Lines changed: 114 additions & 0 deletions

File tree

MTRX.xcodeproj/project.pbxproj

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,7 @@
235235
CB0F67F0CE7E8FF7E3AF19BB /* TrialManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88FA70838C8C6F1CBC1F85B4 /* TrialManager.swift */; };
236236
5B031DA597A70E826A01EA36 /* MtrxComponents.swift in Sources */ = {isa = PBXBuildFile; fileRef = 020D53A9EC3924EDB2BC9913 /* MtrxComponents.swift */; };
237237
A1A1A1A1A1A1A1A1A1A1A6D1 /* StoryAvatar.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1A1A1A1A1A1A1A1A1A1A6D2 /* StoryAvatar.swift */; };
238+
D7D7D7D7D7D7D7D7D7D7D7A1 /* GuardianReviewPanel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7D7D7D7D7D7D7D7D7D7D7A2 /* GuardianReviewPanel.swift */; };
238239
72C79D53328FA2323F5E3028 /* SubscriptionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F06E34845F543EEDE7B91C1C /* SubscriptionView.swift */; };
239240
6FA529066511A5865FD08CA9 /* LaunchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 981C9E1F936DA94C2599780D /* LaunchView.swift */; };
240241
F51012B3159498482F002A6A /* NotificationCenterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E2A724376917CF6778A0D91 /* NotificationCenterView.swift */; };
@@ -598,6 +599,7 @@
598599
88FA70838C8C6F1CBC1F85B4 /* TrialManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = "TrialManager.swift"; path = "Core/Subscription/TrialManager.swift"; sourceTree = SOURCE_ROOT; };
599600
020D53A9EC3924EDB2BC9913 /* MtrxComponents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = "MtrxComponents.swift"; path = "UI/Components/MtrxComponents.swift"; sourceTree = SOURCE_ROOT; };
600601
A1A1A1A1A1A1A1A1A1A1A6D2 /* StoryAvatar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = "StoryAvatar.swift"; path = "UI/Components/StoryAvatar.swift"; sourceTree = SOURCE_ROOT; };
602+
D7D7D7D7D7D7D7D7D7D7D7A2 /* GuardianReviewPanel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = "GuardianReviewPanel.swift"; path = "UI/Components/GuardianReviewPanel.swift"; sourceTree = SOURCE_ROOT; };
601603
F06E34845F543EEDE7B91C1C /* SubscriptionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = "SubscriptionView.swift"; path = "UI/Views/Account/SubscriptionView.swift"; sourceTree = SOURCE_ROOT; };
602604
981C9E1F936DA94C2599780D /* LaunchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = "LaunchView.swift"; path = "UI/Views/Launch/LaunchView.swift"; sourceTree = SOURCE_ROOT; };
603605
9E2A724376917CF6778A0D91 /* NotificationCenterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = "NotificationCenterView.swift"; path = "UI/Views/Notifications/NotificationCenterView.swift"; sourceTree = SOURCE_ROOT; };
@@ -1939,6 +1941,7 @@
19391941
CB0F67F0CE7E8FF7E3AF19BB /* TrialManager.swift in Sources */,
19401942
5B031DA597A70E826A01EA36 /* MtrxComponents.swift in Sources */,
19411943
A1A1A1A1A1A1A1A1A1A1A6D1 /* StoryAvatar.swift in Sources */,
1944+
D7D7D7D7D7D7D7D7D7D7D7A1 /* GuardianReviewPanel.swift in Sources */,
19421945
72C79D53328FA2323F5E3028 /* SubscriptionView.swift in Sources */,
19431946
6FA529066511A5865FD08CA9 /* LaunchView.swift in Sources */,
19441947
F51012B3159498482F002A6A /* NotificationCenterView.swift in Sources */,
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
// GuardianReviewPanel.swift
2+
// MTRX — Morpheus pre-transaction guardian: warning/recommend SURFACE (M-GUARDIAN, piece 2)
3+
//
4+
// A PRESENTATION-ONLY panel that shows the advisory observations from
5+
// MorpheusGuardian.review(...) before a money action reaches the hard gate. Per
6+
// MORPHEUS_ADVISOR_SPEC.md and the piece-2 constraints:
7+
//
8+
// • ADVISORY ONLY. This view DISPLAYS observations; it owns no send/sign logic and
9+
// exposes no callback that changes whether an action proceeds. There is no
10+
// "acknowledge to continue", no required tap, no toggle the host must satisfy. The
11+
// user reads, then proceeds (or doesn't); the host's EXISTING gates run unchanged.
12+
// • READ-ONLY CONSUMPTION. It takes a plain [MorpheusGuardian.Observation] value and
13+
// renders it. It never calls an enforcement path and never feeds back into one.
14+
// • HONEST FRAMING. The observations already carry their grounding + price caveats; the
15+
// panel adds an explicit "this is advice, not a block" note so the advisory nature is
16+
// unmistakable. Nothing is shown unless the guardian grounded it.
17+
18+
import SwiftUI
19+
20+
struct GuardianReviewPanel: View {
21+
let observations: [MorpheusGuardian.Observation]
22+
23+
var body: some View {
24+
if !observations.isEmpty {
25+
VStack(alignment: .leading, spacing: Spacing.sm) {
26+
HStack(spacing: Spacing.xs) {
27+
Image(systemName: "shield.lefthalf.filled")
28+
.font(.system(size: 13, weight: .semibold))
29+
.foregroundStyle(Color.accentPrimary)
30+
Text("Morpheus noticed")
31+
.font(.mtrxCaptionBold)
32+
.foregroundStyle(Color.labelSecondary)
33+
}
34+
35+
ForEach(Array(observations.enumerated()), id: \.offset) { _, obs in
36+
GuardianObservationRow(observation: obs)
37+
}
38+
39+
// Advisory, not a gate: the user decides; the existing security checks
40+
// (Face ID, chain lock, fail-closed send) run on every send regardless.
41+
Text("This is advice, not a block — you decide. Your usual security checks still run on every send.")
42+
.font(.mtrxCaption2)
43+
.foregroundStyle(Color.labelTertiary)
44+
.padding(.top, Spacing.xs)
45+
}
46+
.frame(maxWidth: .infinity, alignment: .leading)
47+
.padding(Spacing.md)
48+
.mtrxCardStyle()
49+
.accessibilityElement(children: .contain)
50+
.accessibilityLabel("Morpheus security notes. Advisory only.")
51+
}
52+
}
53+
}
54+
55+
private struct GuardianObservationRow: View {
56+
let observation: MorpheusGuardian.Observation
57+
58+
var body: some View {
59+
HStack(alignment: .top, spacing: Spacing.sm) {
60+
Image(systemName: glyph)
61+
.font(.system(size: 13, weight: .semibold))
62+
.foregroundStyle(tint)
63+
.frame(width: 18)
64+
VStack(alignment: .leading, spacing: 2) {
65+
Text(observation.message)
66+
.font(.mtrxCaption1)
67+
.foregroundStyle(Color.labelPrimary)
68+
if let rec = observation.recommendation {
69+
Text(rec)
70+
.font(.mtrxCaption2)
71+
.foregroundStyle(Color.labelSecondary)
72+
}
73+
}
74+
Spacer(minLength: 0)
75+
}
76+
}
77+
78+
private var glyph: String {
79+
switch observation.severity {
80+
case .info: return "info.circle"
81+
case .caution: return "exclamationmark.triangle"
82+
case .high: return "exclamationmark.octagon"
83+
}
84+
}
85+
private var tint: Color {
86+
switch observation.severity {
87+
case .info: return Color.labelSecondary
88+
case .caution: return Color.statusWarning
89+
case .high: return Color.statusError
90+
}
91+
}
92+
}

UI/Views/Wallet/SendView.swift

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@ struct SendView: View {
2828
@State private var isSending: Bool = false
2929
@State private var sendError: String?
3030
@State private var sentHash: String?
31+
// M-GUARDIAN piece 2: advisory observations shown on the review screen BEFORE the
32+
// gate. Read-only display — they NEVER change whether the send proceeds.
33+
@State private var guardianObservations: [MorpheusGuardian.Observation] = []
3134

3235
private var selectedToken: AppTokenBalance {
3336
guard walletManager.tokens.indices.contains(selectedTokenIndex) else {
@@ -426,6 +429,16 @@ struct SendView: View {
426429
private var sendButton: some View {
427430
Button {
428431
MtrxHaptics.impact(.medium)
432+
// Gather Morpheus's advisory observations (read-only) to SHOW on the review
433+
// screen. This gates nothing — the Confirm & Send button below runs
434+
// sendNative's existing biometric / preflight / broadcast gates regardless.
435+
guardianObservations = isNativeSend
436+
? MorpheusGuardian.reviewSend(
437+
recipient: recipientAddress,
438+
amountUSD: usdEquivalent > 0 ? usdEquivalent : nil,
439+
todayOutgoingUSD: 0, // running daily total isn't tracked here; daily-soft fires only on a single >threshold send (under-fires the cumulative case — it never over-claims)
440+
kind: .nativeSend)
441+
: []
429442
withAnimation(Motion.springDefault) {
430443
showReview = true
431444
}
@@ -458,6 +471,12 @@ struct SendView: View {
458471
}
459472
.padding(.horizontal, Spacing.contentPadding)
460473

474+
// Morpheus's advisory observations — read-only, shown BEFORE the gate. They
475+
// never change whether the send proceeds; Confirm & Send below runs the same
476+
// biometric → preflight → broadcast gates regardless (sendNative is untouched).
477+
GuardianReviewPanel(observations: guardianObservations)
478+
.padding(.horizontal, Spacing.contentPadding)
479+
461480
VStack(spacing: Spacing.ms) {
462481
Button {
463482
MtrxHaptics.impact(.medium)

0 commit comments

Comments
 (0)