"""ShellApp — the main SDK entry point. Usage: from substrate_sdk import ShellApp app = ShellApp(shard_name="my-app", version="1.0.0", capabilities={"network": True}) context = app.register() """ import json import logging import os import socket import uuid from typing import Optional from .chronicle import LAYER_APPLICATION, ShardEmitter, emit_event from .context import ShardContext logger = logging.getLogger(__name__) SHELL_SOCKET = os.environ.get("SUBSTRATE_SHELL_SOCKET", "/run/bascule/shard.sock") REGISTER_TIMEOUT = float(os.environ.get("SUBSTRATE_REGISTER_TIMEOUT", "5.0")) class ShellApp: """A shellbound application instance. Non-fatal everywhere: shell unavailable → dev mode. Emit fails → logged. Never raises due to shell issues. Never blocks startup >5 seconds. """ def __init__( self, shard_name: str, version: str = "0.0.0", capabilities: Optional[dict] = None, language: str = "python", ): self.shard_name = shard_name self.version = version self.capabilities = capabilities or {} self.language = language self._context: Optional[ShardContext] = None self._emitter: Optional[ShardEmitter] = None self._socket: Optional[socket.socket] = None def register(self) -> ShardContext: """Register with bascule shell. Falls back to dev mode if unavailable.""" context = ShardContext( shard_name=self.shard_name, shard_id=str(uuid.uuid4()), language=self.language, version=self.version, ) if os.path.exists(SHELL_SOCKET): try: context = self._register_with_shell(context) except Exception as e: logger.warning("[substrate-sdk] Shell registration failed (dev mode): %s", e) else: logger.info("[substrate-sdk] %s: running without shell — development mode", self.shard_name) self._context = context self._emitter = ShardEmitter(context) self._emitter.emit( "APP_SHARD_STARTED", LAYER_APPLICATION, { "shard_name": self.shard_name, "shard_id": context.shard_id, "shard_did": context.shard_did, "language": self.language, "version": self.version, "capabilities": self.capabilities, "ffc_did": context.ffc_did, "governed": context.is_governed, }, ) return context def shutdown(self, reason: str = "normal") -> None: """Deregister from shell. Emits APP_SHARD_STOPPED.""" if self._emitter and self._context: self._emitter.emit( "APP_SHARD_STOPPED", LAYER_APPLICATION, {"shard_name": self.shard_name, "shard_id": self._context.shard_id, "reason": reason}, ) if self._socket: try: msg = json.dumps({ "type": "SHARD_DEREGISTER", "shard_id": self._context.shard_id if self._context else "", "reason": reason, }) self._socket.sendall((msg + "\n").encode()) except Exception: pass finally: self._socket.close() @property def emitter(self) -> Optional[ShardEmitter]: return self._emitter @property def context(self) -> Optional[ShardContext]: return self._context def _register_with_shell(self, context: ShardContext) -> ShardContext: s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) s.settimeout(REGISTER_TIMEOUT) s.connect(SHELL_SOCKET) msg = json.dumps({ "type": "SHARD_REGISTER", "shard_name": self.shard_name, "version": self.version, "pid": os.getpid(), "capabilities": self.capabilities, "sdk_version": "0.1.0", "language": self.language, }) s.sendall((msg + "\n").encode()) buf = b"" while b"\n" not in buf: chunk = s.recv(4096) if not chunk: break buf += chunk response = json.loads(buf.split(b"\n")[0]) if response.get("type") == "SHARD_REGISTERED": context.shard_did = response.get("shard_did", "") context.svid = response.get("svid", "") context.accord_hash = response.get("accord_hash", "") context.ffc_did = response.get("ffc_did", "") context.is_governed = True self._socket = s logger.info("[substrate-sdk] %s registered. DID: %s", self.shard_name, context.shard_did) return context