Update
This commit is contained in:
@@ -1,348 +0,0 @@
|
||||
# retoor <retoor@molodetz.nl>
|
||||
|
||||
import inspect
|
||||
import os
|
||||
|
||||
import httpx
|
||||
|
||||
from devplacepy.cache import TTLCache
|
||||
from devplacepy_services.base.db_codec import (
|
||||
decode_value,
|
||||
encode_args,
|
||||
is_write,
|
||||
is_write_sql,
|
||||
)
|
||||
|
||||
_SERVICE_URL = os.environ.get("DEVPLACE_DB_SERVICE_URL", "http://127.0.0.1:10601").rstrip("/")
|
||||
_INTERNAL_KEY = os.environ.get("DEVPLACE_GATEWAY_INTERNAL_KEY", "").strip()
|
||||
_CLIENT: httpx.Client | None = None
|
||||
|
||||
# Section 7.3: settings reads tolerate up to 5s staleness. patch_module()
|
||||
# generically RPCs every devplacepy.database call, bypassing the local
|
||||
# TTL cache get_setting/get_int_setting had in-process - without this,
|
||||
# every settings read (rate limiting, maintenance mode, admin dashboards)
|
||||
# pays a full HTTP round trip to the database broker.
|
||||
_SETTINGS_CACHE_TTL_SECONDS = 5
|
||||
_SETTINGS_CACHE = TTLCache(ttl=_SETTINGS_CACHE_TTL_SECONDS, max_size=512)
|
||||
_CACHED_SETTINGS_FNS = frozenset({"get_setting", "get_int_setting"})
|
||||
|
||||
|
||||
def _service_url() -> str:
|
||||
return os.environ.get("DEVPLACE_DB_SERVICE_URL", _SERVICE_URL).rstrip("/")
|
||||
|
||||
|
||||
def _headers() -> dict[str, str]:
|
||||
headers: dict[str, str] = {}
|
||||
key = os.environ.get("DEVPLACE_GATEWAY_INTERNAL_KEY", _INTERNAL_KEY).strip()
|
||||
if key:
|
||||
headers["X-Internal-Key"] = key
|
||||
return headers
|
||||
|
||||
|
||||
def _client() -> httpx.Client:
|
||||
global _CLIENT
|
||||
if _CLIENT is None:
|
||||
_CLIENT = httpx.Client(timeout=30.0)
|
||||
return _CLIENT
|
||||
|
||||
|
||||
def _post(path: str, body: dict) -> object:
|
||||
response = _client().post(
|
||||
f"{_service_url()}/{path.lstrip('/')}",
|
||||
json=body,
|
||||
headers=_headers(),
|
||||
)
|
||||
if response.status_code >= 400:
|
||||
payload = response.json() if response.content else {}
|
||||
message = payload.get("error", "Database service request failed")
|
||||
raise RuntimeError(message)
|
||||
if not response.content:
|
||||
return None
|
||||
return decode_value(response.json())
|
||||
|
||||
|
||||
def _invoke_cached(fn_name: str, args, kwargs):
|
||||
cache_key = f"{fn_name}:{args!r}:{sorted(kwargs.items())!r}"
|
||||
cached = _SETTINGS_CACHE.get(cache_key)
|
||||
if cached is not None:
|
||||
return cached
|
||||
value = _invoke(fn_name, args, kwargs, write=False)
|
||||
_SETTINGS_CACHE.set(cache_key, value)
|
||||
return value
|
||||
|
||||
|
||||
def _invoke(fn_name: str, args, kwargs, *, write: bool = False):
|
||||
encoded_args, encoded_kwargs = encode_args(args, kwargs)
|
||||
payload = {
|
||||
"fn": fn_name,
|
||||
"args": encoded_args,
|
||||
"kwargs": encoded_kwargs,
|
||||
"write": write,
|
||||
}
|
||||
result = _post("internal/invoke", payload)
|
||||
if isinstance(result, dict) and "result" in result:
|
||||
return result["result"]
|
||||
return result
|
||||
|
||||
|
||||
class RemoteSearchClause:
|
||||
def __init__(self, term, fields, author_field=None):
|
||||
self.term = term.strip()
|
||||
self.fields = tuple(fields)
|
||||
self.author_field = author_field
|
||||
|
||||
|
||||
class RemoteUidInClause:
|
||||
def __init__(self, field, uids):
|
||||
self.field = field
|
||||
self.uids = frozenset(uids)
|
||||
|
||||
|
||||
class RemoteTable:
|
||||
def __init__(self, db: "RemoteDb", name: str) -> None:
|
||||
self._db = db
|
||||
self._name = name
|
||||
self._column_cache = None
|
||||
|
||||
def __getattr__(self, name: str):
|
||||
def caller(*args, **kwargs):
|
||||
return self._db._table_op(self._name, name, args, kwargs)
|
||||
|
||||
return caller
|
||||
|
||||
def has_column(self, name: str) -> bool:
|
||||
cache = self._column_cache
|
||||
if cache is None:
|
||||
sample = self.find(_limit=1)
|
||||
row = next(iter(sample), None)
|
||||
cache = set(row.keys()) if row else set()
|
||||
self._column_cache = cache
|
||||
return name in cache
|
||||
|
||||
def count(self, **kwargs):
|
||||
return self._db._table_op(self._name, "count", [], kwargs)
|
||||
|
||||
@property
|
||||
def table(self):
|
||||
return self
|
||||
|
||||
@property
|
||||
def exists(self) -> bool:
|
||||
return self._name in self._db.tables
|
||||
|
||||
class RemoteDb:
|
||||
def __init__(self) -> None:
|
||||
self._tables_cache: list[str] | None = None
|
||||
|
||||
@property
|
||||
def tables(self) -> list[str]:
|
||||
if self._tables_cache is None:
|
||||
result = _post("internal/db-op", {"op": "tables"})
|
||||
self._tables_cache = list(result or [])
|
||||
return self._tables_cache
|
||||
|
||||
def __getitem__(self, name: str) -> RemoteTable:
|
||||
return RemoteTable(self, name)
|
||||
|
||||
def query(self, sql: str, **params):
|
||||
encoded_args, encoded_kwargs = encode_args((sql,), params)
|
||||
result = _post(
|
||||
"internal/db-op",
|
||||
{
|
||||
"op": "query",
|
||||
"args": encoded_args,
|
||||
"kwargs": encoded_kwargs,
|
||||
"write": is_write_sql(sql),
|
||||
},
|
||||
)
|
||||
return result or []
|
||||
|
||||
def _table_op(self, table: str, method: str, args, kwargs, *, write: bool = False):
|
||||
encoded_args, encoded_kwargs = encode_args(args, kwargs)
|
||||
result = _post(
|
||||
"internal/db-op",
|
||||
{
|
||||
"op": "table_op",
|
||||
"table": table,
|
||||
"method": method,
|
||||
"args": encoded_args,
|
||||
"kwargs": encoded_kwargs,
|
||||
"write": write,
|
||||
},
|
||||
)
|
||||
if method in {"insert", "update", "delete"}:
|
||||
self._tables_cache = None
|
||||
return result
|
||||
|
||||
@property
|
||||
def executable(self):
|
||||
return self
|
||||
|
||||
@property
|
||||
def in_transaction(self) -> bool:
|
||||
return False
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc, tb):
|
||||
return False
|
||||
|
||||
|
||||
_LOCAL_REMOTE = frozenset(
|
||||
{
|
||||
"get_table",
|
||||
"refresh_snapshot",
|
||||
"_in_clause",
|
||||
"_now_iso",
|
||||
"text_search_clause",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def _remote_text_search_clause(
|
||||
table, search, fields=("title", "description"), author_field=None
|
||||
):
|
||||
term = (search or "").strip()
|
||||
if not term:
|
||||
return None
|
||||
if type(table).__name__ == "RemoteTable":
|
||||
return RemoteSearchClause(term, fields, author_field)
|
||||
from devplacepy.database.content import text_search_clause as local_clause
|
||||
|
||||
return local_clause(table, search, fields, author_field=author_field)
|
||||
|
||||
|
||||
def _remote_get_table(name: str):
|
||||
import devplacepy.database.core as core
|
||||
|
||||
return core.db[name]
|
||||
|
||||
|
||||
def _remote_refresh_snapshot() -> None:
|
||||
return None
|
||||
|
||||
|
||||
def patch_module(module) -> None:
|
||||
import devplacepy.database as db_module
|
||||
|
||||
for name in db_module.__all__:
|
||||
if name in _LOCAL_REMOTE:
|
||||
continue
|
||||
target = getattr(module, name, None)
|
||||
if target is None or not callable(target):
|
||||
continue
|
||||
if inspect.isclass(target):
|
||||
continue
|
||||
|
||||
def make_wrapper(fn_name: str, fn_write: bool):
|
||||
if fn_name in _CACHED_SETTINGS_FNS:
|
||||
|
||||
def wrapper(*args, **kwargs):
|
||||
return _invoke_cached(fn_name, args, kwargs)
|
||||
|
||||
wrapper.__name__ = fn_name
|
||||
return wrapper
|
||||
|
||||
def wrapper(*args, **kwargs):
|
||||
return _invoke(fn_name, args, kwargs, write=fn_write)
|
||||
|
||||
wrapper.__name__ = fn_name
|
||||
return wrapper
|
||||
|
||||
setattr(module, name, make_wrapper(name, is_write(name)))
|
||||
|
||||
|
||||
def activate() -> None:
|
||||
import devplacepy.database.core as core
|
||||
|
||||
core.db = RemoteDb()
|
||||
import devplacepy.database as db_module
|
||||
|
||||
patch_module(db_module)
|
||||
for submodule_name in (
|
||||
"settings",
|
||||
"users",
|
||||
"relations",
|
||||
"pagination",
|
||||
"soft_delete",
|
||||
"engagement",
|
||||
"usage",
|
||||
"awards",
|
||||
"seo_meta",
|
||||
"activity",
|
||||
"customization",
|
||||
"email",
|
||||
"notifications",
|
||||
"forks",
|
||||
"follows",
|
||||
"deepsearch",
|
||||
"ranking",
|
||||
"comments",
|
||||
"content",
|
||||
"attachments_data",
|
||||
"stats",
|
||||
"schema",
|
||||
):
|
||||
try:
|
||||
submodule = __import__(
|
||||
f"devplacepy.database.{submodule_name}",
|
||||
fromlist=[submodule_name],
|
||||
)
|
||||
except ImportError:
|
||||
continue
|
||||
patch_module(submodule)
|
||||
for external_name in (
|
||||
"devplacepy.services.statistics.tracking",
|
||||
"devplacepy.services.base",
|
||||
"devplacepy.attachments",
|
||||
"devplacepy.project_files",
|
||||
):
|
||||
try:
|
||||
external = __import__(external_name, fromlist=[external_name.split(".")[-1]])
|
||||
except ImportError:
|
||||
continue
|
||||
if hasattr(external, "db"):
|
||||
external.db = RemoteDb()
|
||||
db_module.db = core.db
|
||||
db_module.get_table = _remote_get_table
|
||||
core.get_table = _remote_get_table
|
||||
db_module.refresh_snapshot = _remote_refresh_snapshot
|
||||
core.refresh_snapshot = _remote_refresh_snapshot
|
||||
db_module.text_search_clause = _remote_text_search_clause
|
||||
import devplacepy.database.content as content_module
|
||||
|
||||
content_module.text_search_clause = _remote_text_search_clause
|
||||
for submodule_name in (
|
||||
"settings",
|
||||
"users",
|
||||
"relations",
|
||||
"pagination",
|
||||
"soft_delete",
|
||||
"engagement",
|
||||
"usage",
|
||||
"awards",
|
||||
"seo_meta",
|
||||
"activity",
|
||||
"customization",
|
||||
"email",
|
||||
"notifications",
|
||||
"forks",
|
||||
"follows",
|
||||
"deepsearch",
|
||||
"ranking",
|
||||
"comments",
|
||||
"content",
|
||||
"attachments_data",
|
||||
"stats",
|
||||
"schema",
|
||||
):
|
||||
try:
|
||||
submodule = __import__(
|
||||
f"devplacepy.database.{submodule_name}",
|
||||
fromlist=[submodule_name],
|
||||
)
|
||||
except ImportError:
|
||||
continue
|
||||
if hasattr(submodule, "db"):
|
||||
submodule.db = core.db
|
||||
@@ -1,29 +0,0 @@
|
||||
# retoor <retoor@molodetz.nl>
|
||||
|
||||
import os
|
||||
|
||||
|
||||
def _activate() -> None:
|
||||
if os.environ.get("DEVPLACE_DB_SERVICE") == "1":
|
||||
return
|
||||
if os.environ.get("DEVPLACE_REMOTE_DB") == "1":
|
||||
from devplacepy.database.remote import activate
|
||||
|
||||
activate()
|
||||
|
||||
|
||||
_activate()
|
||||
|
||||
import devplacepy.database as _database
|
||||
|
||||
|
||||
def _remote_table(table) -> bool:
|
||||
return type(table).__name__ == "RemoteTable"
|
||||
|
||||
|
||||
def __getattr__(name: str):
|
||||
return getattr(_database, name)
|
||||
|
||||
|
||||
def __dir__():
|
||||
return sorted(name for name in dir(_database) if not name.startswith("_"))
|
||||
@@ -2,7 +2,7 @@ This file documents the audit log subsystem. Claude Code auto-loads it when a fi
|
||||
|
||||
## Audit log (`services/audit/`)
|
||||
|
||||
**Admin-only, append-only** record of every state-changing action. The authoritative event catalogue is `events.md` (its storage model, relation vocabulary, and per-event specs are authoritative); the catalogue currently spans 223 keys across 38 domains.
|
||||
**Admin-only, append-only** record of every state-changing action. The authoritative event catalogue is `events.md` (its storage model, relation vocabulary, and per-event specs are authoritative); the catalogue currently spans 288 keys across 42 domains.
|
||||
|
||||
### Package layout
|
||||
|
||||
|
||||
@@ -120,6 +120,7 @@ The security hotpatch that used to run per build is now baked into `ppy.Dockerfi
|
||||
- `COPY`s the **sudo superclone** (`files/sudo`) over `/usr/local/bin/sudo` (+ symlink `/usr/bin/sudo`; the real `sudo` package is not installed).
|
||||
- `COPY`s the **`aptroot` fakeroot wrapper** (`files/aptroot`, symlinked over `apt`/`apt-get`/`dpkg` in `/usr/local/bin` so pravda installs system packages without root).
|
||||
- `COPY`s **`pagent`** (`files/pagent`, the stdlib AI agent; reads `DEVPLACE_OPENAI_URL`+`DEVPLACE_API_KEY`, falling back to its public endpoint + `DEEPSEEK_API_KEY`) to `/usr/bin/pagent.py`, plus `files/.vimrc` to `/home/pravda/.vimrc` (whose AI helper - `AiEditSelection` - targets the same gateway as pagent via `DEVPLACE_OPENAI_URL`/`DEVPLACE_API_KEY`, with a public fallback, never `api.openai.com`).
|
||||
- `COPY`s **`dpc`** (`files/dpc`, DevPlace Code, the Claude-Code-class coding agent) to `/usr/bin/dpc`, and **`bot.py`** to `/usr/bin/botje.py`. **`dpc` is the one prebuilt binary in this repository**: a stripped ELF 64-bit x86-64 executable, 3578664 bytes, sha256 `24f7fbb068e8461e085c5d49ea92556afc10452e4610de7784b87650f96377dd`, linked against GCC (Ubuntu 15.2.0-4ubuntu4) 15.2. Its source is NOT in this repository and there is no build recipe here, so unlike every other file in `files/` it cannot be reviewed before it is installed root-owned onto `PATH` in every user container. Record a new checksum here whenever it is replaced.
|
||||
- Evicts any pre-existing uid-1000 user, creates user **`pravda` at `1000:1000`**.
|
||||
- Hands pravda ownership of the toolchain AND the OS package trees (`chown -R pravda` over `/usr/local/lib`, `/usr/local/bin`, `/usr/lib/python3`, `/opt`, `/app`, `/home/pravda`, plus `/usr/lib`, `/usr/bin`, `/usr/sbin`, `/usr/share`, `/usr/include`, `/etc`, `/var/lib`, `/var/cache`, `/var/log`, `/srv` so `apt`/`dpkg` can write; `~/.local/bin` on `PATH`).
|
||||
- Ends on `USER pravda`.
|
||||
|
||||
@@ -42,7 +42,7 @@ def admin_shell_manager() -> ServiceManager:
|
||||
|
||||
Never supervised (no set_lock_owner/supervise call) - used only for
|
||||
describe()/config metadata/key derivation, all of which read or write
|
||||
through db_client to the shared service_state / site_settings tables.
|
||||
through the database layer to the shared service_state / site_settings tables.
|
||||
The owning Tier 3 process is the only one that ever ticks these services.
|
||||
"""
|
||||
global _SHELL_MANAGER
|
||||
|
||||
Reference in New Issue
Block a user