Deploy AIOS web (React glide grid + FastAPI slice)
Browse files- RELEASES.json +7 -1
- VERSION +1 -1
- api/automation_engine.py +21 -0
- api/main.py +78 -9
- api/routes_automation.py +6 -0
RELEASES.json
CHANGED
|
@@ -1,6 +1,12 @@
|
|
| 1 |
{
|
| 2 |
-
"current": "
|
| 3 |
"releases": [
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
{
|
| 5 |
"version": "v32",
|
| 6 |
"sha": "b47b27c",
|
|
|
|
| 1 |
{
|
| 2 |
+
"current": "v33 (2b115e5)",
|
| 3 |
"releases": [
|
| 4 |
+
{
|
| 5 |
+
"version": "v33",
|
| 6 |
+
"sha": "2b115e5",
|
| 7 |
+
"date": "2026-08-21",
|
| 8 |
+
"subject": "W39: stop nonproduction Postgres background egress"
|
| 9 |
+
},
|
| 10 |
{
|
| 11 |
"version": "v32",
|
| 12 |
"sha": "b47b27c",
|
VERSION
CHANGED
|
@@ -1 +1 @@
|
|
| 1 |
-
|
|
|
|
| 1 |
+
v33 (2b115e5)
|
api/automation_engine.py
CHANGED
|
@@ -14551,6 +14551,23 @@ def tick(rt, tenant, now=None, log=print):
|
|
| 14551 |
return started
|
| 14552 |
|
| 14553 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14554 |
def tick_all(now=None, log=print):
|
| 14555 |
"""Every registered tenant. Fail-quiet per tenant: one tenant's broken store must not stop
|
| 14556 |
the others' schedules."""
|
|
@@ -14558,6 +14575,10 @@ def tick_all(now=None, log=print):
|
|
| 14558 |
out = {}
|
| 14559 |
for slug in _rt.known_tenants():
|
| 14560 |
try:
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14561 |
out[slug] = tick(_rt.get_runtime(slug), slug, now=now, log=log)
|
| 14562 |
except Exception as e: # noqa: BLE001
|
| 14563 |
log(f"[aios-auto] tick {slug} skipped: {type(e).__name__}: {e}")
|
|
|
|
| 14551 |
return started
|
| 14552 |
|
| 14553 |
|
| 14554 |
+
def _pg_tick_refusal(tenant_slug, backend=None, write_refusal=None):
|
| 14555 |
+
"""The Pg write wall, before a background tick reads a tenant's whole workspace.
|
| 14556 |
+
|
| 14557 |
+
A non-production Pg Space may display tenant #0, but its recurring automation work has no
|
| 14558 |
+
durable destination there. The tick would otherwise fetch ``user_tables`` several times per
|
| 14559 |
+
minute merely to discover that its first update is refused. This applies to both scheduler
|
| 14560 |
+
and authenticated external-tick callers; HF remains untouched and a granted QA tenant passes.
|
| 14561 |
+
"""
|
| 14562 |
+
actual_backend = backend if backend is not None else os.environ.get("STORE_BACKEND", "hf")
|
| 14563 |
+
if str(actual_backend).strip().lower() != "pg":
|
| 14564 |
+
return None
|
| 14565 |
+
if write_refusal is None:
|
| 14566 |
+
import core.store_pg as _pg
|
| 14567 |
+
write_refusal = _pg.write_refusal
|
| 14568 |
+
return write_refusal(str(tenant_slug))
|
| 14569 |
+
|
| 14570 |
+
|
| 14571 |
def tick_all(now=None, log=print):
|
| 14572 |
"""Every registered tenant. Fail-quiet per tenant: one tenant's broken store must not stop
|
| 14573 |
the others' schedules."""
|
|
|
|
| 14575 |
out = {}
|
| 14576 |
for slug in _rt.known_tenants():
|
| 14577 |
try:
|
| 14578 |
+
refusal = _pg_tick_refusal(slug)
|
| 14579 |
+
if refusal:
|
| 14580 |
+
log(f"[aios-auto] tick {slug} skipped before runtime read: {refusal}")
|
| 14581 |
+
continue
|
| 14582 |
out[slug] = tick(_rt.get_runtime(slug), slug, now=now, log=log)
|
| 14583 |
except Exception as e: # noqa: BLE001
|
| 14584 |
log(f"[aios-auto] tick {slug} skipped: {type(e).__name__}: {e}")
|
api/main.py
CHANGED
|
@@ -603,6 +603,11 @@ def _migrate_env_odoo(why):
|
|
| 603 |
three different operator actions and a blank Keychain page cannot tell them apart.
|
| 604 |
"""
|
| 605 |
try:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 606 |
import routes_keychain as _kc_routes
|
| 607 |
from harness import runtime as _runtime
|
| 608 |
rep = _kc_routes.migrate_env_odoo(_runtime.get_runtime("royal-imports"))
|
|
@@ -690,6 +695,49 @@ def _rebuild_meta_relational(why, rt, previous_fingerprint=None, force_relations
|
|
| 690 |
"changed": True, "applied": False, "written": {}}
|
| 691 |
|
| 692 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 693 |
def _sweep_automation_schemas(why):
|
| 694 |
"""ββ WAVE 32 Β· `W32-T07` β MAKE D'S DECLARATIONS REACH TENANTS THAT ALREADY HAVE THE TABLES.
|
| 695 |
|
|
@@ -744,6 +792,11 @@ def _sweep_automation_schemas(why):
|
|
| 744 |
return
|
| 745 |
for slug in tenants:
|
| 746 |
try:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 747 |
rt = _runtime.get_runtime(slug)
|
| 748 |
# ββ FIXED 2026-08-13 β THIS SWEEP WAS SPAWNING EIGHT DATABASES IN EVERY TENANT.
|
| 749 |
# Owner: *"Database for Royal Imports, why we have fucking IG and TIktok databases."*
|
|
@@ -831,6 +884,11 @@ def _rebuild_odoo_relational(why, previous_meta_fingerprint=None, meta_state_out
|
|
| 831 |
from harness import runtime as _runtime
|
| 832 |
if not _rel.is_royal("royal-imports"):
|
| 833 |
return False
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 834 |
_rt = _runtime.get_runtime("royal-imports")
|
| 835 |
counts = _rel.refresh(_rt, "royal-imports")
|
| 836 |
# ββ AND THEN COMPUTE THE CELLS, which `refresh` does NOT do.
|
|
@@ -979,15 +1037,21 @@ def _store_resync_loop():
|
|
| 979 |
_every = 1800
|
| 980 |
_manual = False
|
| 981 |
try:
|
| 982 |
-
|
| 983 |
-
|
| 984 |
-
|
| 985 |
-
|
| 986 |
-
|
| 987 |
-
|
| 988 |
-
|
| 989 |
-
|
| 990 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 991 |
except Exception: # noqa: BLE001
|
| 992 |
pass
|
| 993 |
_t.sleep(_every)
|
|
@@ -1091,6 +1155,11 @@ def _prewarm():
|
|
| 1091 |
import threading as _th
|
| 1092 |
_th.Thread(target=_seed_and_sync_store, daemon=True, name="store-seed-sync").start()
|
| 1093 |
try:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1094 |
from harness import runtime as _runtime
|
| 1095 |
rt = _runtime.get_runtime("royal-imports")
|
| 1096 |
routes_customers.warm_default(rt)
|
|
|
|
| 603 |
three different operator actions and a blank Keychain page cannot tell them apart.
|
| 604 |
"""
|
| 605 |
try:
|
| 606 |
+
refusal = _pg_write_refusal("royal-imports")
|
| 607 |
+
if refusal:
|
| 608 |
+
print(f"[aios-api] odoo keychain migration skipped ({why}) before user_tables read: "
|
| 609 |
+
f"this deployment cannot persist royal-imports ({refusal})")
|
| 610 |
+
return
|
| 611 |
import routes_keychain as _kc_routes
|
| 612 |
from harness import runtime as _runtime
|
| 613 |
rep = _kc_routes.migrate_env_odoo(_runtime.get_runtime("royal-imports"))
|
|
|
|
| 695 |
"changed": True, "applied": False, "written": {}}
|
| 696 |
|
| 697 |
|
| 698 |
+
def _pg_write_refusal(tenant_slug, backend=None, write_refusal=None):
|
| 699 |
+
"""The existing Pg write wall, available *before* a costly runtime read.
|
| 700 |
+
|
| 701 |
+
A non-production Pg Space may read tenant #0 so it can display the real workspace, but it
|
| 702 |
+
may not persist to it. A relational refresh is a guaranteed write: letting it first call
|
| 703 |
+
``rt.get('user_tables')`` transfers the entire document merely to discover the refusal at
|
| 704 |
+
``rt.update``. This is not a new authorisation rule. It queries the shipping Pg rule before
|
| 705 |
+
the read, and is intentionally a no-op for the HF backend.
|
| 706 |
+
|
| 707 |
+
``backend`` and ``write_refusal`` are narrow injection seams for the no-network W39 witness;
|
| 708 |
+
normal callers use the exact deployment environment and ``core.store_pg.write_refusal``.
|
| 709 |
+
"""
|
| 710 |
+
actual_backend = (backend if backend is not None
|
| 711 |
+
else os.environ.get("STORE_BACKEND", "hf"))
|
| 712 |
+
if str(actual_backend).strip().lower() != "pg":
|
| 713 |
+
return None
|
| 714 |
+
if write_refusal is None:
|
| 715 |
+
import core.store_pg as _pg
|
| 716 |
+
write_refusal = _pg.write_refusal
|
| 717 |
+
return write_refusal(str(tenant_slug))
|
| 718 |
+
|
| 719 |
+
|
| 720 |
+
_NONPROD_PG_SYNC_EVERY_ENV = "AIOS_NONPROD_PG_SYNC_EVERY"
|
| 721 |
+
|
| 722 |
+
|
| 723 |
+
def _nonprod_pg_sync_seconds(preset=None, backend=None, write_refusal=None):
|
| 724 |
+
"""A deployment-only idle cadence for a Pg Space that cannot write tenant #0.
|
| 725 |
+
|
| 726 |
+
Staging deliberately reads the real tenant, but its write wall means it must never persist
|
| 727 |
+
tenant #0's connector setting. Reading that setting every half hour costs a full
|
| 728 |
+
``user_tables`` transfer. The opt-in secret therefore accepts exactly ``daily`` and asks the
|
| 729 |
+
same write wall *before* building a runtime. Production, HF, an unset secret and any typo
|
| 730 |
+
all retain the shipping tenant-configured cadence.
|
| 731 |
+
"""
|
| 732 |
+
raw = preset if preset is not None else os.environ.get(_NONPROD_PG_SYNC_EVERY_ENV, "")
|
| 733 |
+
if str(raw).strip().lower() != "daily":
|
| 734 |
+
return None
|
| 735 |
+
if not _pg_write_refusal("royal-imports", backend=backend,
|
| 736 |
+
write_refusal=write_refusal):
|
| 737 |
+
return None
|
| 738 |
+
return 86400
|
| 739 |
+
|
| 740 |
+
|
| 741 |
def _sweep_automation_schemas(why):
|
| 742 |
"""ββ WAVE 32 Β· `W32-T07` β MAKE D'S DECLARATIONS REACH TENANTS THAT ALREADY HAVE THE TABLES.
|
| 743 |
|
|
|
|
| 792 |
return
|
| 793 |
for slug in tenants:
|
| 794 |
try:
|
| 795 |
+
refusal = _pg_write_refusal(slug)
|
| 796 |
+
if refusal:
|
| 797 |
+
print(f"[aios-api] schema sweep ({why}) {slug}: skipped before user_tables read "
|
| 798 |
+
f"because this deployment cannot persist that tenant ({refusal})")
|
| 799 |
+
continue
|
| 800 |
rt = _runtime.get_runtime(slug)
|
| 801 |
# ββ FIXED 2026-08-13 β THIS SWEEP WAS SPAWNING EIGHT DATABASES IN EVERY TENANT.
|
| 802 |
# Owner: *"Database for Royal Imports, why we have fucking IG and TIktok databases."*
|
|
|
|
| 884 |
from harness import runtime as _runtime
|
| 885 |
if not _rel.is_royal("royal-imports"):
|
| 886 |
return False
|
| 887 |
+
refusal = _pg_write_refusal("royal-imports")
|
| 888 |
+
if refusal:
|
| 889 |
+
print(f"[aios-api] odoo relational rebuild skipped ({why}) before user_tables read: "
|
| 890 |
+
f"this deployment cannot persist royal-imports ({refusal})")
|
| 891 |
+
return False
|
| 892 |
_rt = _runtime.get_runtime("royal-imports")
|
| 893 |
counts = _rel.refresh(_rt, "royal-imports")
|
| 894 |
# ββ AND THEN COMPUTE THE CELLS, which `refresh` does NOT do.
|
|
|
|
| 1037 |
_every = 1800
|
| 1038 |
_manual = False
|
| 1039 |
try:
|
| 1040 |
+
_stage_every = _nonprod_pg_sync_seconds()
|
| 1041 |
+
if _stage_every is not None:
|
| 1042 |
+
_every = _stage_every
|
| 1043 |
+
print("[aios-api] resync cadence uses deployment-only daily override for "
|
| 1044 |
+
"non-production Postgres")
|
| 1045 |
+
else:
|
| 1046 |
+
import odoo_relational as _rel_cad
|
| 1047 |
+
from harness import runtime as _rt_cad
|
| 1048 |
+
# β D-245, KNOWN AND NOT FIXED HERE: the slug is hardcoded, so this reads TENANT #0's
|
| 1049 |
+
# cadence and applies it to a loop that syncs the shared mirror. Correct while one
|
| 1050 |
+
# tenant has Odoo; wrong the moment a second one does. Out of this ticket's scope and
|
| 1051 |
+
# named rather than silently inherited.
|
| 1052 |
+
_secs = _rel_cad.sync_seconds(_rt_cad.get_runtime("royal-imports"))
|
| 1053 |
+
_manual = _secs is None
|
| 1054 |
+
_every = 1800 if _secs is None else max(int(_secs), 60)
|
| 1055 |
except Exception: # noqa: BLE001
|
| 1056 |
pass
|
| 1057 |
_t.sleep(_every)
|
|
|
|
| 1155 |
import threading as _th
|
| 1156 |
_th.Thread(target=_seed_and_sync_store, daemon=True, name="store-seed-sync").start()
|
| 1157 |
try:
|
| 1158 |
+
refusal = _pg_write_refusal("royal-imports")
|
| 1159 |
+
if refusal:
|
| 1160 |
+
print("[aios-api] eager user_tables warm skipped before runtime read: this "
|
| 1161 |
+
f"deployment cannot persist royal-imports ({refusal})")
|
| 1162 |
+
return
|
| 1163 |
from harness import runtime as _runtime
|
| 1164 |
rt = _runtime.get_runtime("royal-imports")
|
| 1165 |
routes_customers.warm_default(rt)
|
api/routes_automation.py
CHANGED
|
@@ -1745,6 +1745,12 @@ async def automation_hook(auto_id: str, token: str, request: Request):
|
|
| 1745 |
"message": "no automation with that id and token"})
|
| 1746 |
for slug in _rt.known_tenants():
|
| 1747 |
try:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1748 |
rt = _rt.get_runtime(slug)
|
| 1749 |
except Exception: # noqa: BLE001
|
| 1750 |
continue
|
|
|
|
| 1745 |
"message": "no automation with that id and token"})
|
| 1746 |
for slug in _rt.known_tenants():
|
| 1747 |
try:
|
| 1748 |
+
# A non-production Pg Space may display tenant #0 but can never persist an
|
| 1749 |
+
# automation webhook there. Skip before building its runtime, exactly as the
|
| 1750 |
+
# scheduler/tick path does; a skipped tenant remains indistinguishable from an
|
| 1751 |
+
# unknown hook to this unauthenticated caller.
|
| 1752 |
+
if engine._pg_tick_refusal(slug):
|
| 1753 |
+
continue
|
| 1754 |
rt = _rt.get_runtime(slug)
|
| 1755 |
except Exception: # noqa: BLE001
|
| 1756 |
continue
|