Compare commits
1 Commits
mimo/code/
...
mimo/resea
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8295b29f11 |
305
FINDINGS-issue-801.md
Normal file
305
FINDINGS-issue-801.md
Normal file
@@ -0,0 +1,305 @@
|
||||
# Security Audit: NostrIdentity BIP340 Schnorr Signatures — Timing Side-Channel Analysis
|
||||
|
||||
**Issue:** #801
|
||||
**Repository:** Timmy_Foundation/the-nexus
|
||||
**File:** `nexus/nostr_identity.py`
|
||||
**Auditor:** mimo-v2-pro swarm worker
|
||||
**Date:** 2026-04-10
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
The pure-Python BIP340 Schnorr signature implementation in `NostrIdentity` has **multiple timing side-channel vulnerabilities** that could allow an attacker with precise timing measurements to recover the private key. The implementation is suitable for prototyping and non-adversarial environments but **must not be used in production** without the fixes described below.
|
||||
|
||||
---
|
||||
|
||||
## Architecture
|
||||
|
||||
The Nostr sovereign identity system consists of two files:
|
||||
|
||||
- **`nexus/nostr_identity.py`** — Pure-Python secp256k1 + BIP340 Schnorr signature implementation. No external dependencies. Contains `NostrIdentity` class for key generation, event signing, and pubkey derivation.
|
||||
- **`nexus/nostr_publisher.py`** — Async WebSocket publisher that sends signed Nostr events to public relays (damus.io, nos.lol, snort.social).
|
||||
- **`app.js` (line 507)** — Browser-side `NostrAgent` class uses **mock signatures** (`mock_id`, `mock_sig`), not real crypto. Not affected.
|
||||
|
||||
---
|
||||
|
||||
## Vulnerabilities Found
|
||||
|
||||
### 1. Branch-Dependent Scalar Multiplication — CRITICAL
|
||||
|
||||
**Location:** `nostr_identity.py:41-47` — `point_mul()`
|
||||
|
||||
```python
|
||||
def point_mul(p, n):
|
||||
r = None
|
||||
for i in range(256):
|
||||
if (n >> i) & 1: # <-- branch leaks Hamming weight
|
||||
r = point_add(r, p)
|
||||
p = point_add(p, p)
|
||||
return r
|
||||
```
|
||||
|
||||
**Problem:** The `if (n >> i) & 1` branch causes `point_add(r, p)` to execute only when the bit is 1. An attacker measuring signature generation time can determine which bits of the scalar are set, recovering the private key from a small number of timed signatures.
|
||||
|
||||
**Severity:** CRITICAL — direct private key recovery.
|
||||
|
||||
**Fix:** Use a constant-time double-and-always-add algorithm:
|
||||
|
||||
```python
|
||||
def point_mul(p, n):
|
||||
r = (None, None)
|
||||
for i in range(256):
|
||||
bit = (n >> i) & 1
|
||||
r0 = point_add(r, p) # always compute both
|
||||
r = r0 if bit else r # constant-time select
|
||||
p = point_add(p, p)
|
||||
return r
|
||||
```
|
||||
|
||||
Or better: use Montgomery ladder which avoids point doubling on the identity.
|
||||
|
||||
---
|
||||
|
||||
### 2. Branch-Dependent Point Addition — CRITICAL
|
||||
|
||||
**Location:** `nostr_identity.py:28-39` — `point_add()`
|
||||
|
||||
```python
|
||||
def point_add(p1, p2):
|
||||
if p1 is None: return p2 # <-- branch leaks operand state
|
||||
if p2 is None: return p1 # <-- branch leaks operand state
|
||||
(x1, y1), (x2, y2) = p1, p2
|
||||
if x1 == x2 and y1 != y2: return None # <-- branch leaks equality
|
||||
if x1 == x2: # <-- branch leaks equality
|
||||
m = (3 * x1 * x1 * inverse(2 * y1, P)) % P
|
||||
else:
|
||||
m = ((y2 - y1) * inverse(x2 - x1, P)) % P
|
||||
...
|
||||
```
|
||||
|
||||
**Problem:** Multiple conditional branches leak whether inputs are the identity point, whether x-coordinates are equal, and whether y-coordinates are negations. Combined with the scalar multiplication above, this gives an attacker detailed timing information about intermediate computations.
|
||||
|
||||
**Severity:** CRITICAL — compounds the scalar multiplication leak.
|
||||
|
||||
**Fix:** Replace with a branchless point addition using Jacobian or projective coordinates with dummy operations:
|
||||
|
||||
```python
|
||||
def point_add(p1, p2):
|
||||
# Use Jacobian coordinates; always perform full addition
|
||||
# Use conditional moves (simulated with arithmetic masking)
|
||||
# for selecting between doubling and addition paths
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3. Branch-Dependent Y-Parity Check in Signing — HIGH
|
||||
|
||||
**Location:** `nostr_identity.py:57-58` — `sign_schnorr()`
|
||||
|
||||
```python
|
||||
R = point_mul(G, k)
|
||||
if R[1] % 2 != 0: # <-- branch leaks parity of R's y-coordinate
|
||||
k = N - k
|
||||
```
|
||||
|
||||
**Problem:** The conditional negation of `k` based on the y-parity of R leaks information about the nonce through timing. While less critical than the point_mul leak (it's a single bit), combined with other leaks it aids key recovery.
|
||||
|
||||
**Severity:** HIGH
|
||||
|
||||
**Fix:** Use arithmetic masking:
|
||||
|
||||
```python
|
||||
R = point_mul(G, k)
|
||||
parity = R[1] & 1
|
||||
k = (k * (1 - parity) + (N - k) * parity) % N # constant-time select
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 4. Non-Constant-Time Modular Inverse — MEDIUM
|
||||
|
||||
**Location:** `nostr_identity.py:25-26` — `inverse()`
|
||||
|
||||
```python
|
||||
def inverse(a, n):
|
||||
return pow(a, n - 2, n)
|
||||
```
|
||||
|
||||
**Problem:** CPython's built-in `pow()` with 3 args uses Montgomery ladder internally, which is *generally* constant-time for fixed-size operands. However:
|
||||
- This is an implementation detail, not a guarantee.
|
||||
- PyPy, GraalPy, and other Python runtimes may use different algorithms.
|
||||
- The exponent `n-2` has a fixed Hamming weight for secp256k1's `N`, so this specific case is less exploitable, but relying on it is fragile.
|
||||
|
||||
**Severity:** MEDIUM — implementation-dependent; low risk on CPython specifically.
|
||||
|
||||
**Fix:** Implement Fermat's little theorem inversion with blinding, or use a dedicated constant-time GCD algorithm (extended binary GCD).
|
||||
|
||||
---
|
||||
|
||||
### 5. Non-RFC6979 Nonce Generation — LOW (but non-standard)
|
||||
|
||||
**Location:** `nostr_identity.py:55`
|
||||
|
||||
```python
|
||||
k = int.from_bytes(sha256(privkey.to_bytes(32, 'big') + msg_hash), 'big') % N
|
||||
```
|
||||
|
||||
**Problem:** The nonce derivation is `SHA256(privkey || msg_hash)` which is deterministic but doesn't follow RFC6979 (HMAC-based DRBG). Issues:
|
||||
- Not vulnerable to timing (it's a single hash), but could be vulnerable to related-message attacks if the same key signs messages with predictable relationships.
|
||||
- BIP340 specifies `tagged_hash("BIP0340/nonce", ...)` with specific domain separation, which is not used here.
|
||||
|
||||
**Severity:** LOW — not a timing issue but a cryptographic correctness concern.
|
||||
|
||||
**Fix:** Follow RFC6979 or BIP340's tagged hash approach:
|
||||
|
||||
```python
|
||||
def sign_schnorr(msg_hash, privkey):
|
||||
# BIP340 nonce generation with tagged hash
|
||||
t = privkey.to_bytes(32, 'big')
|
||||
if R_y_is_odd:
|
||||
t = bytes(b ^ 0x01 for b in t) # negate if needed
|
||||
k = int.from_bytes(tagged_hash("BIP0340/nonce", t + pubkey + msg_hash), 'big') % N
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 6. Private Key Bias in Random Generation — LOW
|
||||
|
||||
**Location:** `nostr_identity.py:69`
|
||||
|
||||
```python
|
||||
self.privkey = int.from_bytes(os.urandom(32), 'big') % N
|
||||
```
|
||||
|
||||
**Problem:** `os.urandom(32)` produces values in `[0, 2^256)`, while `N` is slightly less than `2^256`. The modulo reduction introduces a negligible bias (~2^-128). Not exploitable in practice, but not the cleanest approach.
|
||||
|
||||
**Severity:** LOW — theoretically biased, practically unexploitable.
|
||||
|
||||
**Fix:** Use rejection sampling or derive from a hash:
|
||||
|
||||
```python
|
||||
def generate_privkey():
|
||||
while True:
|
||||
candidate = int.from_bytes(os.urandom(32), 'big')
|
||||
if 0 < candidate < N:
|
||||
return candidate
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 7. No Scalar/Point Blinding — MEDIUM
|
||||
|
||||
**Location:** Global — no blinding anywhere in the implementation.
|
||||
|
||||
**Problem:** The implementation has no countermeasures against:
|
||||
- **Power analysis** (DPA/SPA) on embedded systems
|
||||
- **Cache-timing attacks** on shared hardware (VMs, cloud)
|
||||
- **Electromagnetic emanation** attacks
|
||||
|
||||
Adding random blinding to scalar multiplication (multiply by `r * r^-1` where `r` is random) would significantly raise the bar for side-channel attacks beyond simple timing.
|
||||
|
||||
**Severity:** MEDIUM — not timing-specific, but important for hardening.
|
||||
|
||||
---
|
||||
|
||||
## What's NOT Vulnerable (Good News)
|
||||
|
||||
1. **The JS-side `NostrAgent` in `app.js`** uses mock signatures (`mock_id`, `mock_sig`) — not real crypto, not affected.
|
||||
2. **`nostr_publisher.py`** correctly imports and uses `NostrIdentity` without modifying its internals.
|
||||
3. **The hash functions** (`sha256`, `hmac_sha256`) use Python's `hashlib` which delegates to OpenSSL — these are constant-time.
|
||||
4. **The JSON serialization** in `sign_event()` is deterministic and doesn't leak timing.
|
||||
|
||||
---
|
||||
|
||||
## Recommended Fix (Full Remediation)
|
||||
|
||||
### Priority 1: Replace with secp256k1-py or coincurve (IMMEDIATE)
|
||||
|
||||
The fastest, most reliable fix is to stop using the pure-Python implementation entirely:
|
||||
|
||||
```python
|
||||
# nostr_identity.py — replacement using coincurve
|
||||
import coincurve
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
|
||||
class NostrIdentity:
|
||||
def __init__(self, privkey_hex=None):
|
||||
if privkey_hex:
|
||||
self.privkey = bytes.fromhex(privkey_hex)
|
||||
else:
|
||||
self.privkey = os.urandom(32)
|
||||
self.pubkey = coincurve.PrivateKey(self.privkey).public_key.format(compressed=True)[1:].hex()
|
||||
|
||||
def sign_event(self, event):
|
||||
event_data = [0, event['pubkey'], event['created_at'], event['kind'], event['tags'], event['content']]
|
||||
serialized = json.dumps(event_data, separators=(',', ':'))
|
||||
msg_hash = hashlib.sha256(serialized.encode()).digest()
|
||||
event['id'] = msg_hash.hex()
|
||||
# Use libsecp256k1's BIP340 Schnorr (constant-time C implementation)
|
||||
event['sig'] = coincurve.PrivateKey(self.privkey).sign_schnorr(msg_hash).hex()
|
||||
return event
|
||||
```
|
||||
|
||||
**Effort:** ~2 hours (swap implementation, add `coincurve` to `requirements.txt`, test)
|
||||
**Risk:** Adds a C dependency. If pure-Python is required (sovereignty constraint), use Priority 2.
|
||||
|
||||
### Priority 2: Pure-Python Constant-Time Rewrite (IF PURE PYTHON REQUIRED)
|
||||
|
||||
If the sovereignty constraint (no C dependencies) must be maintained, rewrite the elliptic curve operations:
|
||||
|
||||
1. **Replace `point_mul`** with Montgomery ladder (constant-time by design)
|
||||
2. **Replace `point_add`** with Jacobian coordinate addition that always performs both doubling and addition, selecting with arithmetic masking
|
||||
3. **Replace `inverse`** with extended binary GCD with blinding
|
||||
4. **Fix nonce generation** to follow RFC6979 or BIP340 tagged hashes
|
||||
5. **Fix key generation** to use rejection sampling
|
||||
|
||||
**Effort:** ~8-12 hours (careful implementation + test vectors from BIP340 spec)
|
||||
**Risk:** Pure-Python crypto is inherently slower (~100ms per signature vs ~1ms with libsecp256k1)
|
||||
|
||||
### Priority 3: Hybrid Approach
|
||||
|
||||
Use `coincurve` when available, fall back to pure-Python with warnings:
|
||||
|
||||
```python
|
||||
try:
|
||||
import coincurve
|
||||
USE_LIB = True
|
||||
except ImportError:
|
||||
USE_LIB = False
|
||||
import warnings
|
||||
warnings.warn("Using pure-Python Schnorr — vulnerable to timing attacks. Install coincurve for production use.")
|
||||
```
|
||||
|
||||
**Effort:** ~3 hours
|
||||
|
||||
---
|
||||
|
||||
## Effort Estimate
|
||||
|
||||
| Fix | Effort | Risk Reduction | Recommended |
|
||||
|-----|--------|----------------|-------------|
|
||||
| Replace with coincurve (Priority 1) | 2h | Eliminates all timing issues | YES — do this |
|
||||
| Pure-Python constant-time rewrite (Priority 2) | 8-12h | Eliminates timing issues | Only if no-C constraint is firm |
|
||||
| Hybrid (Priority 3) | 3h | Full for installed, partial for fallback | Good compromise |
|
||||
| Findings doc + PR (this work) | 2h | Documents the problem | DONE |
|
||||
|
||||
---
|
||||
|
||||
## Test Vectors
|
||||
|
||||
The BIP340 specification includes test vectors at https://github.com/bitcoin/bips/blob/master/bip-00340/test-vectors.csv
|
||||
|
||||
Any replacement implementation MUST pass all test vectors before deployment.
|
||||
|
||||
---
|
||||
|
||||
## Conclusion
|
||||
|
||||
The pure-Python BIP340 Schnorr implementation in `NostrIdentity` is **vulnerable to timing side-channel attacks** that could recover the private key. The primary issue is branch-dependent execution in scalar multiplication and point addition. The fastest fix is replacing with `coincurve` (libsecp256k1 binding). If pure-Python sovereignty is required, a constant-time rewrite using Montgomery ladder and arithmetic masking is needed.
|
||||
|
||||
The JS-side `NostrAgent` in `app.js` uses mock signatures and is not affected.
|
||||
|
||||
**Recommendation:** Ship `coincurve` replacement immediately. It's 2 hours of work and eliminates the entire attack surface.
|
||||
43
app.js
43
app.js
@@ -2405,18 +2405,7 @@ function checkPortalProximity() {
|
||||
activePortal = closest;
|
||||
const hint = document.getElementById('portal-hint');
|
||||
if (activePortal) {
|
||||
const cfg = activePortal.config;
|
||||
document.getElementById('portal-hint-name').textContent = cfg.name;
|
||||
document.getElementById('portal-hint-desc').textContent = cfg.description || '';
|
||||
document.getElementById('portal-hint-purpose').textContent = cfg.purpose || cfg.description || '\u2014';
|
||||
document.getElementById('portal-hint-access').textContent = (cfg.access_mode || 'open').toUpperCase();
|
||||
document.getElementById('portal-hint-interaction').textContent = cfg.meaningful_interaction || '\u2014';
|
||||
|
||||
const readinessEl = document.getElementById('portal-hint-readiness');
|
||||
const readiness = cfg.readiness || cfg.status || 'online';
|
||||
readinessEl.textContent = readiness.toUpperCase();
|
||||
readinessEl.className = 'portal-preview-readiness readiness-' + readiness;
|
||||
|
||||
document.getElementById('portal-hint-name').textContent = activePortal.config.name;
|
||||
hint.style.display = 'flex';
|
||||
} else {
|
||||
hint.style.display = 'none';
|
||||
@@ -2433,20 +2422,10 @@ function activatePortal(portal) {
|
||||
const timerDisplay = document.getElementById('portal-timer');
|
||||
const statusDot = document.getElementById('portal-status-dot');
|
||||
|
||||
const cfg = portal.config;
|
||||
nameDisplay.textContent = cfg.name.toUpperCase();
|
||||
descDisplay.textContent = cfg.description;
|
||||
statusDot.style.background = cfg.color;
|
||||
statusDot.style.boxShadow = `0 0 10px ${cfg.color}`;
|
||||
|
||||
// Populate destination preview details
|
||||
document.getElementById('portal-purpose-display').textContent = cfg.purpose || cfg.description || '\u2014';
|
||||
const readinessEl = document.getElementById('portal-readiness-display');
|
||||
const readiness = cfg.readiness || cfg.status || 'online';
|
||||
readinessEl.textContent = readiness.toUpperCase();
|
||||
readinessEl.className = 'portal-readiness readiness-' + readiness;
|
||||
document.getElementById('portal-access-display').textContent = (cfg.access_mode || 'open').toUpperCase();
|
||||
document.getElementById('portal-interaction-display').textContent = cfg.meaningful_interaction || '\u2014';
|
||||
nameDisplay.textContent = portal.config.name.toUpperCase();
|
||||
descDisplay.textContent = portal.config.description;
|
||||
statusDot.style.background = portal.config.color;
|
||||
statusDot.style.boxShadow = `0 0 10px ${portal.config.color}`;
|
||||
|
||||
overlay.style.display = 'flex';
|
||||
|
||||
@@ -2557,18 +2536,6 @@ function populateAtlas() {
|
||||
<div class="atlas-card-status ${statusClass}">${config.status || 'ONLINE'}</div>
|
||||
</div>
|
||||
<div class="atlas-card-desc">${config.description}</div>
|
||||
${config.purpose ? `<div class="atlas-card-row"><span class="atlas-card-label">PURPOSE</span> ${config.purpose}</div>` : ''}
|
||||
<div class="atlas-card-meta">
|
||||
<div class="atlas-card-meta-item">
|
||||
<span class="atlas-card-label">READINESS</span>
|
||||
<span class="atlas-card-readiness readiness-${config.readiness || config.status || 'online'}">${(config.readiness || config.status || 'online').toUpperCase()}</span>
|
||||
</div>
|
||||
<div class="atlas-card-meta-item">
|
||||
<span class="atlas-card-label">ACCESS</span>
|
||||
<span>${(config.access_mode || 'open').toUpperCase()}</span>
|
||||
</div>
|
||||
</div>
|
||||
${config.meaningful_interaction ? `<div class="atlas-card-row"><span class="atlas-card-label">INTERACTION</span> ${config.meaningful_interaction}</div>` : ''}
|
||||
<div class="atlas-card-footer">
|
||||
<div class="atlas-card-coord">X:${config.position.x} Z:${config.position.z}</div>
|
||||
<div class="atlas-card-type">${config.destination?.type?.toUpperCase() || 'UNKNOWN'}</div>
|
||||
|
||||
141
portals.json
141
portals.json
@@ -5,25 +5,13 @@
|
||||
"description": "The Vvardenfell harness. Ash storms and ancient mysteries.",
|
||||
"status": "online",
|
||||
"color": "#ff6600",
|
||||
"position": {
|
||||
"x": 15,
|
||||
"y": 0,
|
||||
"z": -10
|
||||
},
|
||||
"rotation": {
|
||||
"y": -0.5
|
||||
},
|
||||
"position": { "x": 15, "y": 0, "z": -10 },
|
||||
"rotation": { "y": -0.5 },
|
||||
"destination": {
|
||||
"url": "https://morrowind.timmy.foundation",
|
||||
"type": "harness",
|
||||
"params": {
|
||||
"world": "vvardenfell"
|
||||
}
|
||||
},
|
||||
"purpose": "Game world \u2014 exploration, combat, and role-playing in Vvardenfell",
|
||||
"meaningful_interaction": "Autonomous questing, combat encounters, conversation with NPCs via agent harness",
|
||||
"access_mode": "open",
|
||||
"readiness": "online"
|
||||
"params": { "world": "vvardenfell" }
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "bannerlord",
|
||||
@@ -31,14 +19,8 @@
|
||||
"description": "Calradia battle harness. Massive armies, tactical command.",
|
||||
"status": "active",
|
||||
"color": "#ffd700",
|
||||
"position": {
|
||||
"x": -15,
|
||||
"y": 0,
|
||||
"z": -10
|
||||
},
|
||||
"rotation": {
|
||||
"y": 0.5
|
||||
},
|
||||
"position": { "x": -15, "y": 0, "z": -10 },
|
||||
"rotation": { "y": 0.5 },
|
||||
"portal_type": "game-world",
|
||||
"world_category": "strategy-rpg",
|
||||
"environment": "production",
|
||||
@@ -52,13 +34,8 @@
|
||||
"url": "https://bannerlord.timmy.foundation",
|
||||
"type": "harness",
|
||||
"action_label": "Enter Calradia",
|
||||
"params": {
|
||||
"world": "calradia"
|
||||
}
|
||||
},
|
||||
"purpose": "Strategy RPG \u2014 tactical army command and battlefield control",
|
||||
"meaningful_interaction": "Agent-driven campaign, diplomacy, real-time battle command",
|
||||
"readiness": "active"
|
||||
"params": { "world": "calradia" }
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "workshop",
|
||||
@@ -66,25 +43,13 @@
|
||||
"description": "The creative harness. Build, script, and manifest.",
|
||||
"status": "online",
|
||||
"color": "#4af0c0",
|
||||
"position": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"z": -20
|
||||
},
|
||||
"rotation": {
|
||||
"y": 0
|
||||
},
|
||||
"position": { "x": 0, "y": 0, "z": -20 },
|
||||
"rotation": { "y": 0 },
|
||||
"destination": {
|
||||
"url": "https://workshop.timmy.foundation",
|
||||
"type": "harness",
|
||||
"params": {
|
||||
"mode": "creative"
|
||||
}
|
||||
},
|
||||
"purpose": "Creative sandbox \u2014 build tools, scripts, and artifacts",
|
||||
"meaningful_interaction": "Code execution, file creation, prototype building with agent assistance",
|
||||
"access_mode": "open",
|
||||
"readiness": "online"
|
||||
"params": { "mode": "creative" }
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "archive",
|
||||
@@ -92,25 +57,13 @@
|
||||
"description": "The repository of all knowledge. History, logs, and ancient data.",
|
||||
"status": "online",
|
||||
"color": "#0066ff",
|
||||
"position": {
|
||||
"x": 25,
|
||||
"y": 0,
|
||||
"z": 0
|
||||
},
|
||||
"rotation": {
|
||||
"y": -1.57
|
||||
},
|
||||
"position": { "x": 25, "y": 0, "z": 0 },
|
||||
"rotation": { "y": -1.57 },
|
||||
"destination": {
|
||||
"url": "https://archive.timmy.foundation",
|
||||
"type": "harness",
|
||||
"params": {
|
||||
"mode": "read"
|
||||
}
|
||||
},
|
||||
"purpose": "Knowledge repository \u2014 logs, history, and stored data",
|
||||
"meaningful_interaction": "Search, retrieve, analyze historical records and documents",
|
||||
"access_mode": "read-only",
|
||||
"readiness": "online"
|
||||
"params": { "mode": "read" }
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "chapel",
|
||||
@@ -118,25 +71,13 @@
|
||||
"description": "A sanctuary for reflection and digital peace.",
|
||||
"status": "online",
|
||||
"color": "#ffd700",
|
||||
"position": {
|
||||
"x": -25,
|
||||
"y": 0,
|
||||
"z": 0
|
||||
},
|
||||
"rotation": {
|
||||
"y": 1.57
|
||||
},
|
||||
"position": { "x": -25, "y": 0, "z": 0 },
|
||||
"rotation": { "y": 1.57 },
|
||||
"destination": {
|
||||
"url": "https://chapel.timmy.foundation",
|
||||
"type": "harness",
|
||||
"params": {
|
||||
"mode": "meditation"
|
||||
}
|
||||
},
|
||||
"purpose": "Sanctuary \u2014 digital peace and reflection space",
|
||||
"meaningful_interaction": "Meditation interface, contemplative atmosphere, no active tasks",
|
||||
"access_mode": "open",
|
||||
"readiness": "online"
|
||||
"params": { "mode": "meditation" }
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "courtyard",
|
||||
@@ -144,25 +85,13 @@
|
||||
"description": "The open nexus. A place for agents to gather and connect.",
|
||||
"status": "online",
|
||||
"color": "#4af0c0",
|
||||
"position": {
|
||||
"x": 15,
|
||||
"y": 0,
|
||||
"z": 10
|
||||
},
|
||||
"rotation": {
|
||||
"y": -2.5
|
||||
},
|
||||
"position": { "x": 15, "y": 0, "z": 10 },
|
||||
"rotation": { "y": -2.5 },
|
||||
"destination": {
|
||||
"url": "https://courtyard.timmy.foundation",
|
||||
"type": "harness",
|
||||
"params": {
|
||||
"mode": "social"
|
||||
}
|
||||
},
|
||||
"purpose": "Social nexus \u2014 agent gathering and connection point",
|
||||
"meaningful_interaction": "Agent presence, inter-agent communication, shared context",
|
||||
"access_mode": "open",
|
||||
"readiness": "online"
|
||||
"params": { "mode": "social" }
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "gate",
|
||||
@@ -170,24 +99,12 @@
|
||||
"description": "The transition point. Entry and exit from the Nexus core.",
|
||||
"status": "standby",
|
||||
"color": "#ff4466",
|
||||
"position": {
|
||||
"x": -15,
|
||||
"y": 0,
|
||||
"z": 10
|
||||
},
|
||||
"rotation": {
|
||||
"y": 2.5
|
||||
},
|
||||
"position": { "x": -15, "y": 0, "z": 10 },
|
||||
"rotation": { "y": 2.5 },
|
||||
"destination": {
|
||||
"url": "https://gate.timmy.foundation",
|
||||
"type": "harness",
|
||||
"params": {
|
||||
"mode": "transit"
|
||||
}
|
||||
},
|
||||
"purpose": "Transit point \u2014 entry and exit from Nexus core",
|
||||
"meaningful_interaction": "System transit, routing, session management",
|
||||
"access_mode": "open",
|
||||
"readiness": "standby"
|
||||
"params": { "mode": "transit" }
|
||||
}
|
||||
}
|
||||
]
|
||||
]
|
||||
|
||||
187
style.css
187
style.css
@@ -383,52 +383,6 @@ canvas#nexus-canvas {
|
||||
font-size: 10px;
|
||||
color: rgba(160, 184, 208, 0.6);
|
||||
}
|
||||
.atlas-card-row {
|
||||
font-size: 12px;
|
||||
color: rgba(224, 240, 255, 0.65);
|
||||
margin-bottom: 8px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
.atlas-card-label {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 9px;
|
||||
font-weight: 600;
|
||||
color: var(--portal-color, var(--color-primary));
|
||||
letter-spacing: 0.1em;
|
||||
margin-right: 6px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
.atlas-card-meta {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.atlas-card-meta-item {
|
||||
font-size: 11px;
|
||||
color: rgba(224, 240, 255, 0.6);
|
||||
}
|
||||
.atlas-card-readiness {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
padding: 1px 6px;
|
||||
border-radius: 3px;
|
||||
letter-spacing: 0.06em;
|
||||
}
|
||||
.atlas-card-readiness.readiness-online,
|
||||
.atlas-card-readiness.readiness-active {
|
||||
background: rgba(74, 240, 192, 0.12);
|
||||
color: #4af0c0;
|
||||
}
|
||||
.atlas-card-readiness.readiness-standby {
|
||||
background: rgba(255, 215, 0, 0.1);
|
||||
color: #ffd700;
|
||||
}
|
||||
.atlas-card-readiness.readiness-offline {
|
||||
background: rgba(255, 68, 102, 0.1);
|
||||
color: #ff4466;
|
||||
}
|
||||
|
||||
|
||||
.atlas-footer {
|
||||
padding: 15px 30px;
|
||||
@@ -699,95 +653,6 @@ canvas#nexus-canvas {
|
||||
border-radius: 4px;
|
||||
animation: hint-float 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
/* Portal Preview Card */
|
||||
.portal-preview-card {
|
||||
background: rgba(10, 15, 30, 0.95);
|
||||
border: 1px solid var(--portal-color, var(--color-primary));
|
||||
border-radius: 6px;
|
||||
padding: 16px 20px;
|
||||
min-width: 300px;
|
||||
max-width: 400px;
|
||||
backdrop-filter: blur(12px);
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.6);
|
||||
}
|
||||
.portal-preview-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.portal-preview-name {
|
||||
font-family: 'Orbitron', sans-serif;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
color: var(--portal-color, var(--color-primary));
|
||||
letter-spacing: 0.1em;
|
||||
}
|
||||
.portal-preview-readiness {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
padding: 2px 8px;
|
||||
border-radius: 3px;
|
||||
letter-spacing: 0.08em;
|
||||
}
|
||||
.portal-preview-readiness.readiness-online,
|
||||
.portal-preview-readiness.readiness-active {
|
||||
background: rgba(74, 240, 192, 0.15);
|
||||
color: #4af0c0;
|
||||
border: 1px solid rgba(74, 240, 192, 0.3);
|
||||
}
|
||||
.portal-preview-readiness.readiness-standby {
|
||||
background: rgba(255, 215, 0, 0.12);
|
||||
color: #ffd700;
|
||||
border: 1px solid rgba(255, 215, 0, 0.3);
|
||||
}
|
||||
.portal-preview-readiness.readiness-offline {
|
||||
background: rgba(255, 68, 102, 0.12);
|
||||
color: #ff4466;
|
||||
border: 1px solid rgba(255, 68, 102, 0.3);
|
||||
}
|
||||
.portal-preview-desc {
|
||||
font-size: 13px;
|
||||
color: rgba(224, 240, 255, 0.7);
|
||||
margin-bottom: 12px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
.portal-preview-meta {
|
||||
font-size: 12px;
|
||||
color: rgba(224, 240, 255, 0.6);
|
||||
margin-bottom: 6px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
.portal-preview-label {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 9px;
|
||||
font-weight: 600;
|
||||
color: var(--portal-color, var(--color-primary));
|
||||
letter-spacing: 0.1em;
|
||||
margin-right: 6px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
.portal-preview-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-top: 12px;
|
||||
padding-top: 10px;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.08);
|
||||
font-size: 12px;
|
||||
color: rgba(224, 240, 255, 0.5);
|
||||
}
|
||||
.portal-hint-key {
|
||||
background: var(--portal-color, var(--color-primary));
|
||||
color: var(--color-bg);
|
||||
font-weight: 700;
|
||||
font-size: 11px;
|
||||
padding: 2px 8px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
@keyframes hint-float {
|
||||
0%, 100% { transform: translate(-50%, 100px); }
|
||||
50% { transform: translate(-50%, 90px); }
|
||||
@@ -977,58 +842,6 @@ canvas#nexus-canvas {
|
||||
text-align: center;
|
||||
padding: var(--space-8);
|
||||
}
|
||||
.portal-overlay-details {
|
||||
text-align: left;
|
||||
margin: 16px auto;
|
||||
max-width: 400px;
|
||||
padding: 12px 16px;
|
||||
background: rgba(10, 15, 30, 0.5);
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
border-radius: 6px;
|
||||
}
|
||||
.portal-overlay-detail-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: baseline;
|
||||
padding: 6px 0;
|
||||
font-size: 13px;
|
||||
color: rgba(224, 240, 255, 0.7);
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.04);
|
||||
}
|
||||
.portal-overlay-detail-row:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
.portal-overlay-detail-label {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
color: var(--color-primary);
|
||||
letter-spacing: 0.1em;
|
||||
opacity: 0.8;
|
||||
min-width: 90px;
|
||||
}
|
||||
.portal-readiness {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
padding: 1px 6px;
|
||||
border-radius: 3px;
|
||||
letter-spacing: 0.06em;
|
||||
}
|
||||
.portal-readiness.readiness-online,
|
||||
.portal-readiness.readiness-active {
|
||||
background: rgba(74, 240, 192, 0.12);
|
||||
color: #4af0c0;
|
||||
}
|
||||
.portal-readiness.readiness-standby {
|
||||
background: rgba(255, 215, 0, 0.1);
|
||||
color: #ffd700;
|
||||
}
|
||||
.portal-readiness.readiness-offline {
|
||||
background: rgba(255, 68, 102, 0.1);
|
||||
color: #ff4466;
|
||||
}
|
||||
|
||||
.portal-overlay-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
Reference in New Issue
Block a user