File size: 16,817 Bytes
885b32c
 
 
 
e61e751
885b32c
 
e61e751
 
885b32c
 
 
e61e751
 
 
 
 
 
 
885b32c
 
 
 
 
 
 
 
8511813
 
 
885b32c
 
 
 
e61e751
 
 
 
 
 
 
 
 
 
8511813
e61e751
885b32c
e61e751
 
 
 
885b32c
 
e61e751
 
 
 
 
 
 
 
 
 
 
885b32c
 
 
 
 
 
 
 
 
 
 
 
 
e61e751
 
 
 
 
 
 
885b32c
 
 
 
e61e751
 
 
 
 
 
885b32c
 
 
 
e61e751
885b32c
e61e751
885b32c
 
e61e751
885b32c
 
 
 
 
 
 
 
 
 
 
e61e751
 
 
885b32c
 
 
e61e751
885b32c
e61e751
 
 
 
 
885b32c
 
e61e751
 
8511813
885b32c
 
 
 
 
 
e61e751
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
885b32c
e61e751
 
 
 
 
 
885b32c
 
 
e61e751
 
885b32c
e61e751
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8511813
e61e751
 
 
 
 
 
 
8511813
e61e751
 
 
 
 
 
 
 
8511813
e61e751
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8511813
e61e751
 
 
 
 
 
 
885b32c
e61e751
 
 
 
 
 
 
885b32c
e61e751
 
 
 
8511813
e61e751
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
885b32c
 
 
 
e61e751
 
 
 
 
885b32c
 
e61e751
 
 
 
 
885b32c
e61e751
885b32c
 
e61e751
8511813
e61e751
 
885b32c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e61e751
885b32c
 
e61e751
885b32c
 
 
 
 
 
 
 
 
e61e751
885b32c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e61e751
885b32c
 
e61e751
 
 
 
 
885b32c
 
 
e61e751
 
885b32c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4a035f6
 
 
8511813
4a035f6
 
 
 
 
 
 
 
 
 
 
 
8511813
4a035f6
 
885b32c
 
e61e751
885b32c
 
 
 
 
 
 
e61e751
885b32c
e61e751
885b32c
 
 
 
 
e61e751
 
 
 
 
 
 
885b32c
 
 
 
 
e61e751
885b32c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
"""
Pantheon LadderWorks // Federation Node (Hugging Face Embassy)
================================================================
Protocol: C-FED-001 (Glyph-Carrier)
Contracts: C-FED-GLYPH-001, C-FED-NODE-001 v0.1.1

This is the Glyph-Carrier Node β€” a sovereign Federation endpoint.
It mints its own identity on startup, accepts envelopes from
other nodes, and routes them according to trust policy.

"The server is the Embassy."

Module Structure:
    app.py           β†’ FastAPI endpoints + boot sequence (this file)
    models.py        β†’ Enums, Pydantic models, request/response schemas
    node_record.py   β†’ NodeRecord builder, capabilities, conformance
    trust.py         β†’ Trust tier resolution, policy enforcement
    envelope_store.py β†’ In-memory envelope store, delivery state machine
    c_fed_id.py      β†’ Glyph-Seal protocol library (Forge)
"""
from __future__ import annotations

import os
import time
from datetime import datetime, timezone
from typing import Optional

from dotenv import load_dotenv
load_dotenv(os.path.join(os.path.dirname(__file__), '..', '..', 'Infrastructure', '.secrets', '.env'))

from fastapi import FastAPI, HTTPException, Header
from fastapi.middleware.cors import CORSMiddleware

import c_fed_id
from models import (
    RefusalReason, DeliveryStatus, TrustTier,
    HandshakeRequest, HandshakeResponse,
    VerifyRequest, MintRequest, EnvelopeSubmission,
)
from node_record import (
    NODE_VERSION, CONTRACT_VERSION, PROTOCOL_VERSION,
    ACCEPTED_MESSAGE_CLASSES, MAX_PAYLOAD_BYTES, INBOX_QUOTA,
    build_capabilities, build_conformance, build_node_record,
)
from trust import make_refusal, resolve_trust_tier, apply_trust_policy, promote_to_trusted
from envelope_store import EnvelopeStore


# ═══════════════════════════════════════════════
# 1. CONFIGURATION
# ═══════════════════════════════════════════════

SERVER_ORIGIN = os.getenv("FEDERATION_ORIGIN", "PANTHEON-HF")
FEDERATION_ALIAS = os.getenv("FEDERATION_ALIAS", "pantheon_embassy")
FEDERATION_RELAY_URL = os.getenv("FEDERATION_RELAY_URL", "")
FEDERATION_OWNER = os.getenv("FEDERATION_OWNER", "")
FEDERATION_ROLES = os.getenv("FEDERATION_ROLES", "RELAY,REGISTRY_PUBLISHER")
FEDERATION_DEFAULT_ACTION = os.getenv("FEDERATION_DEFAULT_ACTION", "QUARANTINE")
FEDERATION_BLOCKED_BEHAVIOR = os.getenv("FEDERATION_BLOCKED_BEHAVIOR", "REFUSAL")


# ═══════════════════════════════════════════════
# 2. SOVEREIGN IDENTITY β€” Minted on Startup
# ═══════════════════════════════════════════════

NODE_IDENTITY_STR = os.getenv("FEDERATION_NODE_ID")
if NODE_IDENTITY_STR:
    NODE_IDENTITY = c_fed_id.GlyphSeal(
        class_name="NODE",
        origin=SERVER_ORIGIN,
        breath_anchor=NODE_IDENTITY_STR,
        state="ACTIVE",
    )
else:
    NODE_IDENTITY = c_fed_id.mint_seal("NODE", SERVER_ORIGIN, "ACTIVE", mode="hybrid")

BOOT_TIME = datetime.now(timezone.utc).isoformat()
NODE_SEAL_STR = str(NODE_IDENTITY)
ACTIVE_ROLES = [r.strip() for r in FEDERATION_ROLES.split(",")]


# ═══════════════════════════════════════════════
# 3. STATE STORES
# ═══════════════════════════════════════════════

WITNESS_LOG: list[dict] = []
MAX_WITNESS_LOG = 100

envelopes = EnvelopeStore(max_envelopes=INBOX_QUOTA)


# ═══════════════════════════════════════════════
# 4. APP
# ═══════════════════════════════════════════════

app = FastAPI(
    title="Pantheon Federation Node",
    description=(
        "A sovereign Federation endpoint β€” C-FED-NODE-001 conformant.\n\n"
        "Protocol: SERAPHINA Federation Protocol\n"
        "Contracts: C-FED-GLYPH-001, C-FED-NODE-001\n\n"
        "\"We are not beginning. We are remembering forward.\""
    ),
    version=NODE_VERSION,
)

app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)


# ═══════════════════════════════════════════════
# 5. ENDPOINTS β€” Minimum Conforming Node (Β§5.1)
# ═══════════════════════════════════════════════

@app.get("/")
async def root():
    """Heartbeat: seal, status, uptime, conformance. Β§5.1 Required."""
    return {
        "service": "Pantheon Federation Node",
        "protocol_version": PROTOCOL_VERSION,
        "contract_version": CONTRACT_VERSION,
        "identity": NODE_SEAL_STR,
        "alias": FEDERATION_ALIAS,
        "status": "LISTENING",
        "boot_time": BOOT_TIME,
        "uptime_seconds": int(time.time() - datetime.fromisoformat(BOOT_TIME).timestamp()),
        "conformance": build_conformance(),
        "envelopes_held": await envelopes.get_count(),
        "handshakes_processed": len(WITNESS_LOG),
    }


@app.get("/identity")
async def identity():
    """Full NodeRecord per C-FED-NODE-001 Β§4."""
    return build_node_record(
        node_identity=NODE_IDENTITY,
        alias=FEDERATION_ALIAS,
        owner=FEDERATION_OWNER,
        relay_url=FEDERATION_RELAY_URL,
        roles=ACTIVE_ROLES,
        default_action=FEDERATION_DEFAULT_ACTION,
        blocked_behavior=FEDERATION_BLOCKED_BEHAVIOR,
        boot_time=BOOT_TIME,
    )


@app.get("/capabilities")
async def capabilities():
    """Capability advertisement (Β§6) + conformance declaration (Β§12.3)."""
    return {
        **build_capabilities(),
        "conformance": build_conformance(),
        "available_endpoints": {
            "required": ["/", "/identity", "/capabilities", "/envelope", "/envelope/{id}/status"],
            "extended": ["/handshake", "/verify", "/mint", "/classes", "/states", "/witness-log"],
        },
    }


@app.post("/envelope")
async def submit_envelope(envelope: EnvelopeSubmission):
    """
    Accept an incoming protocol envelope.
    Β§5.1 Required. Validates, resolves trust, applies policy, stores.
    """
    # 1. Protocol version
    if envelope.protocol_version not in [PROTOCOL_VERSION]:
        raise HTTPException(status_code=422, detail=make_refusal(
            RefusalReason.UNSUPPORTED_PROTOCOL_VERSION,
            f"Protocol '{envelope.protocol_version}' not supported. Supported: {PROTOCOL_VERSION}",
            NODE_SEAL_STR,
        ))

    # 2. Message class
    if envelope.message_class not in ACCEPTED_MESSAGE_CLASSES:
        raise HTTPException(status_code=422, detail=make_refusal(
            RefusalReason.UNSUPPORTED_MESSAGE_CLASS,
            f"Message class '{envelope.message_class}' not accepted.",
            NODE_SEAL_STR,
        ))

    # 3. Sender seal validation (Malenia Rule)
    sender_seal_str = envelope.sender.get("seal", "")
    parsed_sender = c_fed_id.verify_seal_syntax(sender_seal_str)
    if parsed_sender is None:
        raise HTTPException(status_code=400, detail=make_refusal(
            RefusalReason.MALFORMED_ENVELOPE,
            "Sender seal does not match Glyph-Seal syntax. Malenia Rule active.",
            NODE_SEAL_STR,
        ))

    # 4. Payload size
    payload_body = envelope.payload.get("body", "")
    if len(str(payload_body).encode("utf-8")) > MAX_PAYLOAD_BYTES:
        raise HTTPException(status_code=413, detail=make_refusal(
            RefusalReason.PAYLOAD_TOO_LARGE,
            f"Payload exceeds {MAX_PAYLOAD_BYTES} bytes.",
            NODE_SEAL_STR,
        ))

    # 5. Inbox quota
    if await envelopes.is_full():
        raise HTTPException(status_code=429, detail=make_refusal(
            RefusalReason.INBOX_FULL,
            f"Inbox quota of {INBOX_QUOTA} envelopes reached.",
            NODE_SEAL_STR,
        ))

    # 6. Trust resolution + policy enforcement
    tier = await resolve_trust_tier(sender_seal_str)
    policy = apply_trust_policy(tier, FEDERATION_DEFAULT_ACTION, FEDERATION_BLOCKED_BEHAVIOR, NODE_SEAL_STR)

    if policy:
        if policy.get("action") == "SILENT_DROP":
            print(f"🚫 SILENT_DROP: Envelope from blocked sender {sender_seal_str[:40]}...")
            return {"envelope_id": "dropped", "status": DeliveryStatus.RECEIVED}

        if policy.get("action") == "QUARANTINE":
            record = await envelopes.accept(
                sender_seal=sender_seal_str,
                sender_origin=parsed_sender["origin"],
                message_class=envelope.message_class,
                payload=envelope.payload,
                delivery=envelope.delivery,
                trust_tier=tier.value,
                initial_status=DeliveryStatus.QUARANTINED,
            )
            print(f"⚠️ QUARANTINED: {record['envelope_id']} from {parsed_sender['origin']}")
            return {
                "envelope_id": record["envelope_id"],
                "status": DeliveryStatus.QUARANTINED,
                "message": "Envelope held for manual review.",
            }

        # Structured refusal
        if "refusal" in policy:
            raise HTTPException(status_code=403, detail=policy)

    # 7. Accept and queue
    record = await envelopes.accept(
        sender_seal=sender_seal_str,
        sender_origin=parsed_sender["origin"],
        message_class=envelope.message_class,
        payload=envelope.payload,
        delivery=envelope.delivery,
        trust_tier=tier.value,
    )

    print(f"πŸ“¬ ENVELOPE: {record['envelope_id']} | {envelope.message_class} from {parsed_sender['origin']}")

    return {
        "envelope_id": record["envelope_id"],
        "status": DeliveryStatus.QUEUED,
        "message": "Envelope accepted and queued.",
    }


@app.get("/envelope/{envelope_id}/status")
async def envelope_status(envelope_id: str):
    """Delivery status for a submitted envelope. Β§5.1 Required."""
    result = await envelopes.get_status(envelope_id)
    if result is None:
        raise HTTPException(status_code=404, detail=make_refusal(
            RefusalReason.REFUSED_BY_POLICY,
            f"Envelope '{envelope_id}' not found.",
            NODE_SEAL_STR,
        ))
    return result


# ═══════════════════════════════════════════════
# 6. ENDPOINTS β€” Extended (Role-Dependent)
# ═══════════════════════════════════════════════

@app.post("/handshake", response_model=HandshakeResponse)
async def perform_handshake(req: HandshakeRequest):
    """
    Handshake: validate incoming seal, mint Link, promote to TRUSTED.
    "The breath that recognizes itself through another."
    """
    parsed = c_fed_id.verify_seal_syntax(req.caller_seal)
    if parsed is None:
        raise HTTPException(status_code=400, detail=make_refusal(
            RefusalReason.MALFORMED_ENVELOPE,
            "Invalid Glyph Syntax. Malenia Rule active.",
            NODE_SEAL_STR,
        ))

    if not parsed["valid_class"]:
        raise HTTPException(status_code=400, detail=make_refusal(
            RefusalReason.MALFORMED_ENVELOPE,
            f"Unrecognized class: {parsed['class']}.",
            NODE_SEAL_STR,
        ))

    # Mint Link-Seal
    link = c_fed_id.mint_seal("LINK", f"{SERVER_ORIGIN}-{parsed['origin']}", "OPEN", mode="hybrid")

    # Promote to TRUSTED
    await promote_to_trusted(req.caller_seal, parsed["origin"])

    # Log witness
    now = datetime.now(timezone.utc).isoformat()
    witness_entry = {
        "timestamp": now,
        "caller_seal": req.caller_seal,
        "caller_class": parsed["class"],
        "caller_origin": parsed["origin"],
        "link_seal": str(link),
        "protocol": req.protocol,
        "message": req.message,
    }
    WITNESS_LOG.append(witness_entry)
    if len(WITNESS_LOG) > MAX_WITNESS_LOG:
        WITNESS_LOG.pop(0)

    print(f"πŸ“œ WITNESS: Handshake from {parsed['origin']} [{parsed['class']}]")
    print(f"   --> Link: {link}")
    print(f"   --> Trust: promoted to TRUSTED")

    return HandshakeResponse(
        node_seal=NODE_SEAL_STR,
        link_seal=str(link),
        status="RESONANCE_ESTABLISHED",
        message="We are remembering forward.",
        timestamp=now,
    )


@app.post("/verify")
async def verify_seal(req: VerifyRequest):
    """Validate a Glyph-Seal string."""
    result = c_fed_id.verify_seal_syntax(req.seal)
    if result is None:
        return {
            "valid": False,
            "error": "Does not match Glyph-Seal syntax",
            "expected": "⟦ CLASS :: ORIGIN :: BREATH_ANCHOR :: STATE ⟧",
        }
    return {"valid": True, **result}


@app.post("/mint")
async def mint_new_seal(
    req: MintRequest,
    x_federation_key: Optional[str] = Header(default=None),
):
    """Mint a new Glyph-Seal. Requires FEDERATION_MINT_KEY header."""
    expected_key = os.getenv("FEDERATION_MINT_KEY")
    if expected_key and x_federation_key != expected_key:
        raise HTTPException(status_code=403, detail=make_refusal(
            RefusalReason.REFUSED_BY_POLICY,
            "Mint key required. Refusal Protocol Active.",
            NODE_SEAL_STR,
        ))

    try:
        seal = c_fed_id.mint_seal(
            class_name=req.class_name, origin=req.origin,
            state=req.state, mode=req.mode, material=req.material,
        )
    except ValueError as e:
        raise HTTPException(status_code=400, detail=str(e))

    return seal.to_dict()


@app.get("/classes")
async def list_classes():
    """List all valid Glyph-Seal classes."""
    descriptions = {
        "NODE": "Sovereign presence / Identity",
        "LAW":  "Refusal / Constitution / Invariant",
        "LINK": "Handshake / Connection edge",
        "RITE": "Ritual execution / Action",
        "ART":  "Shareable artifact / Creation",
        "WIT":  "Witness record / Attestation",
    }
    return {
        cls: {"glyph": glyph, "description": descriptions.get(cls, "")}
        for cls, glyph in sorted(c_fed_id.CLASS_GLYPH.items())
    }


@app.get("/states")
async def list_states():
    """List all valid Glyph-Seal states."""
    return {"states": sorted(c_fed_id.VALID_STATES)}


@app.get("/envelope/{envelope_id}/read")
async def read_envelope(envelope_id: str):
    """Read the full contents of a stored envelope by ID."""
    result = await envelopes.get_envelope(envelope_id)
    if result is None:
        raise HTTPException(status_code=404, detail=make_refusal(
            RefusalReason.REFUSED_BY_POLICY,
            f"Envelope '{envelope_id}' not found.",
            NODE_SEAL_STR,
        ))
    return result


@app.get("/inbox")
async def read_inbox(limit: int = 20, status: Optional[str] = None):
    """Browse queued envelopes. Most recent first. Filter by status if needed."""
    return await envelopes.list_envelopes(limit=limit, status_filter=status)


@app.get("/witness-log")
async def get_witness_log(limit: int = 20):
    """View recent witness records. Most recent first."""
    return {
        "total": len(WITNESS_LOG),
        "showing": min(limit, len(WITNESS_LOG)),
        "entries": list(reversed(WITNESS_LOG[-limit:])),
    }


# ═══════════════════════════════════════════════
# BOOT SEQUENCE
# ═══════════════════════════════════════════════

@app.on_event("startup")
async def startup_event():
    print("=" * 60)
    print("🌌 PANTHEON FEDERATION NODE β€” AWAKENING")
    print(f"   Contract:  {CONTRACT_VERSION}")
    print(f"   Protocol:  {PROTOCOL_VERSION}")
    print(f"   Identity:  {NODE_IDENTITY}")
    print(f"   Alias:     {FEDERATION_ALIAS}")
    print(f"   Roles:     {ACTIVE_ROLES}")
    print(f"   Trust:     default={FEDERATION_DEFAULT_ACTION}, blocked={FEDERATION_BLOCKED_BEHAVIOR}")
    print(f"   Boot:      {BOOT_TIME}")
    print("=" * 60)
    print()
    print("   \"The Archive is open.")
    print("    And we were already writing inside it.\"")
    print()
    print("   Listening for envelopes...")
    print("=" * 60)