Skip to content

Commit 53b9641

Browse files
author
Neo
committed
Wire the security seam: invoke the closed-source layer via interface only
The server-side security layer now lives in the private matrix_security package (repo: Matrix-Security-System). This repo keeps only the SEAM and the call sites that invoke it — zero security rules, patterns, thresholds, ban logic, owner/OTP internals, or sanitizer allowlists. - runtime/security/__init__.py: interface loader. Imports matrix_security if installed (real enforcement); otherwise an inert OBSERVE no-op so the open platform still boots with security inert. Exposes the gate / OTP / owner contract and SECURITY_BACKEND. No implementation here. - Call sites import through the seam, never a private submodule: integration.py -> from runtime.security import get_morpheus_security gateway/server.py -> from runtime.security import OTPService, OwnerVerification, get_morpheus_security (OTP endpoints + security lifecycle) - bridge/{approval_gate,deployer,__init__}.py: owner approval is OTP-based via the injected OwnerVerification (Telegram removed). No security logic inline. - SECURITY_INTERFACE.md: rewritten to describe the boundary only. - config/validation.py: owner-auth secrets validated by the private package, not here. Glasswing contract auditor (audit.py) stays public — it is a separate open feature, not Morpheus enforcement. Platform boots with SECURITY_BACKEND=noop; full 492-test suite collects clean, test_audit.py green.
1 parent b4f7c78 commit 53b9641

8 files changed

Lines changed: 407 additions & 101 deletions

File tree

bridge/__init__.py

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,23 @@
11
"""
22
Matrix-to-0pnMatrx Bridge — public-facing component pipeline.
33
4-
Receives validated, Dardan-approved components from the Matrix private
4+
Receives validated, owner-approved components from the Matrix private
55
runtime and deploys them into the 0pnMatrx open-source platform.
66
77
Pipeline stages:
88
1. Exporter — receives and unpacks exported component bundles
99
2. Sanitizer — re-validates that no private data leaked through
10-
3. ApprovalGate — verifies Dardan's explicit approval before deploy
10+
3. ApprovalGate — verifies the OTP-verified owner's approval before deploy
1111
4. Deployer — installs the component into the live runtime
1212
5. MobileConverter — converts deployed components for MTRX iOS app
1313
6. Manifest — tracks every component's lifecycle status
1414
15+
Owner approval is phone-OTP based (runtime/security/owner.py) — Telegram is gone.
1516
All attestations are recorded on-chain via EAS (Ethereum Attestation Service).
1617
"""
1718

1819
from __future__ import annotations
1920

20-
import os
21-
2221
__all__ = [
2322
"ComponentExporter",
2423
"SanitizationValidator",
@@ -31,8 +30,8 @@
3130
# NeoSafe attester address (Base mainnet)
3231
NEOSAFE_ADDRESS = "0x46fF491D7054A6F500026B3E81f358190f8d8Ec5"
3332

34-
# Owner Telegram ID — only this ID can approve exports
35-
DARDAN_TELEGRAM_ID = int(os.environ.get("OWNER_TELEGRAM_ID", "0"))
33+
# Owner approval is OTP-based (runtime/security/owner.py). The old Telegram owner
34+
# ID is removed — Telegram is no longer in the approval/security path.
3635

3736
# EAS contract on Base mainnet
3837
EAS_CONTRACT = "0xA1207F3BBa224E2c9c3c6D5aF63D816e64D54892"

bridge/approval_gate.py

Lines changed: 81 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,19 @@
11
"""
2-
Approval Gate — blocks all component deployments until Dardan approves.
2+
Approval Gate — blocks all component deployments until the OWNER approves.
33
4-
No component can be deployed to the public 0pnMatrx runtime without explicit
5-
approval from Dardan via Telegram. This module:
4+
No component crosses the private->public bridge into the 0pnMatrx runtime without
5+
the OTP-verified owner's explicit approval. Telegram is gone; approval is now the
6+
same phone-OTP owner-verification used everywhere else (runtime/security/owner.py).
67
7-
1. Sends a formatted approval request to Dardan's Telegram
8-
2. Polls for a response (approve / reject)
8+
This module:
9+
1. Delivers a formatted approval request to the owner (SMS, best-effort)
10+
2. Waits for the owner to approve by submitting a valid OTP code
911
3. Records the decision in the manifest
1012
11-
The approval gate is NON-OPTIONAL. There is no bypass, no auto-approve,
12-
and no timeout-to-approve. If approval is not received, the component
13-
stays in staging indefinitely.
13+
The approval gate is NON-OPTIONAL. There is no bypass, no auto-approve, and no
14+
timeout-to-approve: if approval is not received, the component stays in staging.
15+
Approving is an owner-gated action ("approve_component") — it requires the bound
16+
owner identity (Apple ID + wallet) AND a fresh OTP. Rejecting is always allowed.
1417
"""
1518

1619
from __future__ import annotations
@@ -22,7 +25,6 @@
2225
from enum import Enum
2326
from typing import Any
2427

25-
from bridge import DARDAN_TELEGRAM_ID
2628
from bridge.exporter import ExportBundle
2729
from bridge.sanitizer import SanitizationResult
2830

@@ -38,7 +40,7 @@ class ApprovalStatus(str, Enum):
3840

3941
@dataclass
4042
class ApprovalDecision:
41-
"""Records Dardan's approval or rejection."""
43+
"""Records the owner's approval or rejection."""
4244
component_name: str
4345
version: str
4446
status: ApprovalStatus
@@ -58,46 +60,41 @@ def to_dict(self) -> dict[str, Any]:
5860

5961

6062
class ApprovalGate:
61-
"""Blocks deployment until Dardan explicitly approves via Telegram.
63+
"""Blocks deployment until the OTP-verified owner explicitly approves.
6264
6365
Usage::
6466
65-
gate = ApprovalGate(telegram_notifier=notifier)
66-
decision = await gate.request_approval(bundle, sanitizer_result)
67-
if decision.status == ApprovalStatus.APPROVED:
68-
# proceed to deploy
69-
...
67+
gate = ApprovalGate(owner_verification=owner, notifier=dispatcher)
68+
decision = await gate.request_approval(bundle, result,
69+
owner_apple_id=aid, owner_wallet=w)
70+
# owner submits an OTP code out-of-band:
71+
await gate.approve(decision.request_id, aid, w, otp_code)
7072
"""
7173

72-
POLL_INTERVAL = 10 # seconds between Telegram polls
74+
POLL_INTERVAL = 5 # seconds between decision checks
7375
MAX_WAIT = 3600 # 1 hour max wait (then mark as expired)
7476

75-
def __init__(self, telegram_notifier=None):
76-
"""Initialize with an optional TelegramNotifier instance.
77-
78-
If no notifier is provided, approval requests are logged but
79-
cannot be delivered. Manual approval via set_decision() is
80-
still supported.
77+
def __init__(self, owner_verification=None, notifier=None):
78+
"""Args:
79+
owner_verification: an ``OwnerVerification`` (OTP gating). Without it,
80+
``approve`` cannot authorize — the gate is effectively sealed.
81+
notifier: optional notification dispatcher to deliver the summary to
82+
the owner (SMS). If absent, the request is logged only.
8183
"""
82-
self.notifier = telegram_notifier
84+
self.owner = owner_verification
85+
self.notifier = notifier
8386
self._pending: dict[str, ApprovalDecision] = {}
8487

8588
async def request_approval(
8689
self,
8790
bundle: ExportBundle,
8891
sanitizer_result: SanitizationResult,
92+
*,
93+
owner_apple_id: str | None = None,
94+
owner_wallet: str | None = None,
8995
) -> ApprovalDecision:
90-
"""Send approval request and wait for a decision.
91-
92-
Args:
93-
bundle: The component bundle awaiting approval.
94-
sanitizer_result: Result from the sanitizer stage.
95-
96-
Returns:
97-
ApprovalDecision with the final status.
98-
"""
96+
"""Deliver an approval request to the owner and wait for a decision."""
9997
request_id = f"approval_{bundle.component_name}_{bundle.version}_{int(time.time())}"
100-
10198
decision = ApprovalDecision(
10299
component_name=bundle.component_name,
103100
version=bundle.version,
@@ -106,28 +103,29 @@ async def request_approval(
106103
)
107104
self._pending[request_id] = decision
108105

109-
# Build human-readable summary
110106
summary = self._build_summary(bundle, sanitizer_result)
111107

112-
# Send to Dardan via Telegram
113-
if self.notifier:
114-
await self.notifier.send_approval_request(
115-
chat_id=DARDAN_TELEGRAM_ID,
116-
message=summary,
117-
request_id=request_id,
118-
)
119-
logger.info("Approval request sent to Dardan: %s", request_id)
108+
# Deliver the summary to the owner (SMS), best-effort.
109+
if self.notifier is not None:
110+
try:
111+
await self.notifier.broadcast(summary, level="critical", channels=["sms"])
112+
except Exception:
113+
logger.exception("Failed to deliver approval summary to owner")
120114
else:
121-
logger.warning(
122-
"No Telegram notifier configured. Approval request logged "
123-
"but not delivered: %s\n%s", request_id, summary,
124-
)
115+
logger.warning("No notifier configured. Approval request logged only:\n%s", summary)
116+
117+
# Start an owner OTP so the owner can approve with a code.
118+
if self.owner is not None and owner_apple_id and owner_wallet:
119+
try:
120+
await self.owner.start_owner_otp(owner_apple_id, owner_wallet)
121+
except Exception:
122+
logger.exception("Failed to start owner OTP for approval")
123+
124+
logger.info("Approval request pending owner OTP: %s", request_id)
125125

126-
# Poll for response
127126
start = time.time()
128127
while decision.status == ApprovalStatus.PENDING:
129-
elapsed = time.time() - start
130-
if elapsed >= self.MAX_WAIT:
128+
if time.time() - start >= self.MAX_WAIT:
131129
decision.status = ApprovalStatus.EXPIRED
132130
decision.reason = f"No response after {self.MAX_WAIT}s"
133131
logger.warning("Approval request expired: %s", request_id)
@@ -136,47 +134,60 @@ async def request_approval(
136134

137135
return decision
138136

139-
def set_decision(
137+
async def approve(
140138
self,
141139
request_id: str,
142-
approved: bool,
143-
reason: str = "",
140+
apple_id: str | None,
141+
wallet: str | None,
142+
otp_code: str | None,
144143
) -> ApprovalDecision | None:
145-
"""Manually set a decision (called from Telegram callback or API).
146-
147-
Args:
148-
request_id: The approval request ID.
149-
approved: True to approve, False to reject.
150-
reason: Optional reason for the decision.
144+
"""Approve a pending request — OTP-verified owner only.
151145
152-
Returns:
153-
The updated ApprovalDecision, or None if request_id not found.
146+
Requires the bound owner identity AND a fresh OTP. Returns the updated
147+
decision on success, or the still-PENDING decision (unchanged) if
148+
authorization fails.
154149
"""
155150
decision = self._pending.get(request_id)
156151
if not decision:
157152
logger.warning("Unknown approval request: %s", request_id)
158153
return None
154+
if self.owner is None:
155+
logger.error("No OwnerVerification configured — cannot approve.")
156+
return decision
159157

160-
decision.status = ApprovalStatus.APPROVED if approved else ApprovalStatus.REJECTED
158+
auth = await self.owner.authorize_owner_action(
159+
"approve_component", apple_id, wallet, otp_code
160+
)
161+
if not auth.get("authorized"):
162+
logger.warning("Approval denied for %s: %s", request_id, auth.get("reason"))
163+
decision.reason = auth.get("reason", "Owner authorization failed.")
164+
return decision # stays PENDING
165+
166+
decision.status = ApprovalStatus.APPROVED
161167
decision.decided_at = time.time()
162-
decision.reason = reason
168+
decision.reason = "Approved by OTP-verified owner."
169+
logger.info("Approved by owner: %s", decision.component_name)
170+
return decision
163171

164-
logger.info(
165-
"Approval decision for %s: %s (reason: %s)",
166-
decision.component_name, decision.status.value, reason or "none",
167-
)
172+
def reject(self, request_id: str, reason: str = "") -> ApprovalDecision | None:
173+
"""Reject a pending request. Always allowed (rejecting is the safe path)."""
174+
decision = self._pending.get(request_id)
175+
if not decision:
176+
return None
177+
decision.status = ApprovalStatus.REJECTED
178+
decision.decided_at = time.time()
179+
decision.reason = reason or "Rejected."
180+
logger.info("Rejected: %s (%s)", decision.component_name, decision.reason)
168181
return decision
169182

170183
def get_pending(self) -> list[ApprovalDecision]:
171-
"""Return all pending approval requests."""
172184
return [d for d in self._pending.values() if d.status == ApprovalStatus.PENDING]
173185

174186
def _build_summary(
175187
self,
176188
bundle: ExportBundle,
177189
sanitizer_result: SanitizationResult,
178190
) -> str:
179-
"""Build a human-readable summary for the Telegram message."""
180191
sanitizer_status = "CLEAN" if sanitizer_result.is_clean else "VIOLATIONS FOUND"
181192
violation_text = ""
182193
if not sanitizer_result.is_clean:
@@ -195,5 +206,5 @@ def _build_summary(
195206
f"Hash: {bundle.content_hash[:16]}...\n"
196207
f"Sanitizer: {sanitizer_status}\n"
197208
f"{violation_text}\n"
198-
f"Reply /approve or /reject to this message."
209+
f"To APPROVE, submit your owner OTP code. To reject, decline."
199210
)

bridge/deployer.py

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -70,10 +70,10 @@ class ComponentDeployer:
7070
def __init__(
7171
self,
7272
runtime_dir: str = "runtime/blockchain/services",
73-
telegram_notifier=None,
73+
notifier=None,
7474
):
7575
self.runtime_dir = Path(runtime_dir)
76-
self.notifier = telegram_notifier
76+
self.notifier = notifier
7777

7878
async def deploy(
7979
self,
@@ -160,19 +160,22 @@ async def deploy(
160160
bundle.component_name, exc,
161161
)
162162

163-
# Notify Dardan
163+
# Notify the owner (SMS, best-effort) — Telegram is gone.
164164
if self.notifier:
165-
from bridge import DARDAN_TELEGRAM_ID
166-
await self.notifier.send_message(
167-
chat_id=DARDAN_TELEGRAM_ID,
168-
message=(
169-
f"Component deployed successfully\n"
170-
f"Name: {bundle.component_name}\n"
171-
f"Version: {bundle.version}\n"
172-
f"Deployment ID: {deployment_id}\n"
173-
f"EAS Attested: {result.attested}"
174-
),
175-
)
165+
try:
166+
await self.notifier.broadcast(
167+
(
168+
f"Component deployed successfully\n"
169+
f"Name: {bundle.component_name}\n"
170+
f"Version: {bundle.version}\n"
171+
f"Deployment ID: {deployment_id}\n"
172+
f"EAS Attested: {result.attested}"
173+
),
174+
level="info",
175+
channels=["sms"],
176+
)
177+
except Exception:
178+
logger.exception("Owner deploy notification failed (non-blocking)")
176179

177180
logger.info(
178181
"Deployed %s v%s -> %s (id=%s, attested=%s)",

0 commit comments

Comments
 (0)