feat: dumb

This commit is contained in:
Vlad Stan
2026-07-01 16:57:18 +03:00
parent 26c31b626d
commit 2cca56aa21
5 changed files with 281 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
"""Extension runtime contracts."""
@@ -0,0 +1,8 @@
from .catalog import get_extension_capability_registry
from .registry import CapabilityDefinition, CapabilityRegistry
__all__ = [
"CapabilityDefinition",
"CapabilityRegistry",
"get_extension_capability_registry",
]
@@ -0,0 +1,120 @@
from .models import (
CreateInvoiceRequest,
CreateInvoiceResponse,
EmptyRequest,
KvGetRequest,
KvGetResponse,
KvListRequest,
KvListResponse,
KvSetRequest,
KvSetResponse,
LogRequest,
LogResponse,
NowResponse,
RandomIdRequest,
RandomIdResponse,
WatchPaymentRequest,
WatchPaymentResponse,
)
from .registry import CapabilityDefinition, CapabilityRegistry
def get_extension_capability_registry() -> CapabilityRegistry:
registry = CapabilityRegistry()
for capability in [
CapabilityDefinition(
id="storage.get",
namespace="storage",
name="Get storage value",
host_name="kv_get",
sdk_name="get",
description="Read one value from the extension storage namespace.",
request_model=KvGetRequest,
response_model=KvGetResponse,
required_permission="ext.storage.read_write",
public_context=True,
),
CapabilityDefinition(
id="storage.set",
namespace="storage",
name="Set storage value",
host_name="kv_set",
sdk_name="set",
description="Write one value to the extension storage namespace.",
request_model=KvSetRequest,
response_model=KvSetResponse,
required_permission="ext.storage.read_write",
),
CapabilityDefinition(
id="storage.list",
namespace="storage",
name="List storage keys",
host_name="kv_list",
sdk_name="list",
description="List keys under a prefix in the extension storage namespace.",
request_model=KvListRequest,
response_model=KvListResponse,
required_permission="ext.storage.read_write",
public_context=True,
),
CapabilityDefinition(
id="wallet.create_invoice",
namespace="wallet",
name="Create invoice",
host_name="create_invoice",
sdk_name="createInvoice",
description="Create an incoming Lightning invoice for an allowed wallet.",
request_model=CreateInvoiceRequest,
response_model=CreateInvoiceResponse,
required_permission="wallet.create_invoice",
public_context=True,
),
CapabilityDefinition(
id="payments.watch",
namespace="payments",
name="Watch payment",
host_name="watch_payment",
sdk_name="watch",
description="Subscribe the extension to a payment state callback.",
request_model=WatchPaymentRequest,
response_model=WatchPaymentResponse,
required_permission="payments.watch",
),
CapabilityDefinition(
id="system.random_id",
namespace="system",
name="Random ID",
host_name="random_id",
sdk_name="id",
description="Create a random extension-local identifier.",
request_model=RandomIdRequest,
response_model=RandomIdResponse,
public_context=True,
),
CapabilityDefinition(
id="system.now",
namespace="system",
name="Current timestamp",
host_name="now",
sdk_name="now",
description="Return the current Unix timestamp.",
request_model=EmptyRequest,
response_model=NowResponse,
public_context=True,
),
CapabilityDefinition(
id="system.log",
namespace="system",
name="Log message",
host_name="log",
sdk_name="log",
description="Write a bounded message to the extension log.",
request_model=LogRequest,
response_model=LogResponse,
public_context=True,
),
]:
registry.register(capability)
return registry
@@ -0,0 +1,76 @@
from typing import Literal
from pydantic import BaseModel, Field
class EmptyRequest(BaseModel):
pass
class KvGetRequest(BaseModel):
key: str = Field(..., min_length=1, max_length=512)
class KvGetResponse(BaseModel):
value: str | None = None
class KvSetRequest(BaseModel):
key: str = Field(..., min_length=1, max_length=512)
value: str = Field(..., max_length=65536)
class KvSetResponse(BaseModel):
ok: bool = True
class KvListRequest(BaseModel):
prefix: str = Field(..., min_length=1, max_length=512)
class KvListResponse(BaseModel):
keys: list[str] = []
class CreateInvoiceRequest(BaseModel):
wallet_id: str = Field(..., min_length=1, max_length=128)
amount_sat: int = Field(..., gt=0)
memo: str = Field(..., max_length=512)
tag: str = Field(..., min_length=1, max_length=64)
extra: dict[str, str] = Field(default_factory=dict)
class CreateInvoiceResponse(BaseModel):
payment_hash: str
payment_request: str
checking_id: str
class WatchPaymentRequest(BaseModel):
payment_hash: str = Field(..., min_length=1, max_length=128)
callback_export: str = Field(..., min_length=1, max_length=128)
class WatchPaymentResponse(BaseModel):
ok: bool = True
class RandomIdRequest(BaseModel):
prefix: str = Field(..., min_length=1, max_length=32)
class RandomIdResponse(BaseModel):
id: str
class NowResponse(BaseModel):
timestamp: int
class LogRequest(BaseModel):
level: Literal["debug", "info", "warning", "error"] = "info"
message: str = Field(..., min_length=1, max_length=2048)
class LogResponse(BaseModel):
ok: bool = True
@@ -0,0 +1,76 @@
from __future__ import annotations
from dataclasses import dataclass
from typing import Any
from pydantic import BaseModel
@dataclass(frozen=True)
class CapabilityDefinition:
id: str
namespace: str
name: str
host_name: str
sdk_name: str
description: str
request_model: type[BaseModel]
response_model: type[BaseModel]
required_permission: str | None = None
public_context: bool = False
@property
def sdk_qualified_name(self) -> str:
return f"{self.namespace}.{self.sdk_name}"
def request_schema(self) -> dict[str, Any]:
return self.request_model.schema(ref_template="#/definitions/{model}")
def response_schema(self) -> dict[str, Any]:
return self.response_model.schema(ref_template="#/definitions/{model}")
def to_codegen_contract(self) -> dict[str, Any]:
return {
"id": self.id,
"namespace": self.namespace,
"name": self.name,
"host_name": self.host_name,
"sdk_name": self.sdk_name,
"sdk_qualified_name": self.sdk_qualified_name,
"description": self.description,
"required_permission": self.required_permission,
"public_context": self.public_context,
"request_schema": self.request_schema(),
"response_schema": self.response_schema(),
}
class CapabilityRegistry:
def __init__(self) -> None:
self._capabilities: dict[str, CapabilityDefinition] = {}
def register(self, capability: CapabilityDefinition) -> None:
if capability.id in self._capabilities:
raise ValueError(f"Capability '{capability.id}' already registered.")
self._capabilities[capability.id] = capability
def get(self, capability_id: str) -> CapabilityDefinition | None:
return self._capabilities.get(capability_id)
def require(self, capability_id: str) -> CapabilityDefinition:
capability = self.get(capability_id)
if not capability:
raise KeyError(f"Unknown capability '{capability_id}'.")
return capability
def all(self) -> list[CapabilityDefinition]:
return sorted(self._capabilities.values(), key=lambda c: c.id)
def by_permission(self, permission_id: str) -> list[CapabilityDefinition]:
return [c for c in self.all() if c.required_permission == permission_id]
def to_codegen_contract(self) -> dict[str, Any]:
return {
"version": 1,
"capabilities": [c.to_codegen_contract() for c in self.all()],
}