fsanyoto commited on
Commit
fdf30e3
Β·
verified Β·
1 Parent(s): 8c719be

Deploy AIOS web (React glide grid + FastAPI slice)

Browse files
RELEASES.json CHANGED
@@ -1,6 +1,12 @@
1
  {
2
- "current": "2b115e5",
3
  "releases": [
 
 
 
 
 
 
4
  {
5
  "version": "v33",
6
  "sha": "2b115e5",
 
1
  {
2
+ "current": "v34 (cb4caa1)",
3
  "releases": [
4
+ {
5
+ "version": "v34",
6
+ "sha": "cb4caa1",
7
+ "date": "2026-08-21",
8
+ "subject": "W39 Staging PostgreSQL egress controls"
9
+ },
10
  {
11
  "version": "v33",
12
  "sha": "2b115e5",
VERSION CHANGED
@@ -1 +1 @@
1
- 2b115e5
 
1
+ v34 (cb4caa1)
api/automation_engine.py CHANGED
@@ -14510,47 +14510,115 @@ def tick(rt, tenant, now=None, log=print):
14510
  started.append(aid)
14511
  except Exception as e: # noqa: BLE001
14512
  log(f"[aios-auto] email poll {tenant}/{aid} failed: {type(e).__name__}: {e}")
14513
- # C7's amendment: date-window metrics advance with the tick, so `today` is never staler
14514
- # than one tick while a scheduler exists. A tenant without metric fields pays a dict scan.
14515
- try:
14516
- refresh_metrics(rt, log=log)
14517
- except Exception as e: # noqa: BLE001
14518
- log(f"[aios-auto] metric refresh {tenant} failed: {type(e).__name__}: {e}")
14519
- # ⭐ 2026-08-07 β€” the relational pass, on the same tick and for the same reason. A rollup on
14520
- # table A goes stale when table B gains a row, and A cannot know that happened; the tick is
14521
- # the only place that sees both. ⚠ SEPARATE try/except from the metrics above deliberately β€”
14522
- # one pass failing must not silently cancel the other, which a shared block would do.
14523
- try:
14524
- refresh_relations(rt, log=log)
14525
- except Exception as e: # noqa: BLE001
14526
- log(f"[aios-auto] relation refresh {tenant} failed: {type(e).__name__}: {e}")
14527
- # ⭐ THE READ-THROUGH ROLLUPS, on the same tick and for a stronger version of the same reason.
14528
- # A linked rollup goes stale when the LINKED TABLE gains a row; a source-backed one goes stale
14529
- # when ODOO does β€” and its window moves on its own besides (a `ytd` column is wrong on 1
14530
- # January without anybody writing anything). The tick is the only place that sees either.
14531
  #
14532
- # β›” ITS OWN try/except, like the two passes above, and the reason is the same one stated
14533
- # there: this pass REFUSES loudly on a truncated group set, and a shared block would let that
14534
- # honest refusal silently cancel the relational pass that had already succeeded.
14535
- #
14536
- # ⚠ IT WRITES NOTHING WHEN IT REFUSES β€” see `rollup_sql`'s header. A half-applied rollup mixes
14537
- # two vintages of one column and looks completely normal, which is why the refusal is total.
14538
- try:
14539
- import rollup_sql
14540
- for _tk in list((rt.get(UT_STORE_KEY) or {})):
14541
- if rollup_sql.source_fields((rt.get(UT_STORE_KEY) or {}).get(_tk) or {}):
14542
- # ⚠ `today` is DEFAULTED INSIDE `compute`, not passed: this scope has no
14543
- # such local, and `refresh_metrics` above resolves it the same way.
14544
- n = rollup_sql.compute(rt, _tk)
14545
- if n:
14546
- log(f"[aios-auto] source rollups {tenant}/{_tk}: {n}")
14547
- except Exception as e: # noqa: BLE001
14548
- log(f"[aios-auto] source rollup {tenant} failed: {type(e).__name__}: {e}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
14549
  if started:
14550
  log(f"[aios-auto] tick {tenant}: started {', '.join(started)}")
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
 
 
14510
  started.append(aid)
14511
  except Exception as e: # noqa: BLE001
14512
  log(f"[aios-auto] email poll {tenant}/{aid} failed: {type(e).__name__}: {e}")
14513
+ # The automation definitions above are a small, separate `automations` bucket: schedule,
14514
+ # pending-result and email work must still be checked every minute. The THREE passes below
14515
+ # instead read the tenant-wide `user_tables` document (28.6 MB measured for tenant #0). On
14516
+ # Postgres its durable revision is a tiny row-column query, so a repeated minute with neither
14517
+ # a workspace write nor a new calendar day must not download that document merely to discover
14518
+ # that every derived value is unchanged. The day remains an independent trigger: date-window
14519
+ # metric and source-rollup cells may change at midnight without a user-table write.
 
 
 
 
 
 
 
 
 
 
 
14520
  #
14521
+ # This is deliberately Pg-only. The HF revision counter is process-local and cannot witness a
14522
+ # second process's write, while Pg's `rev` is durable and schema-shared. An unavailable/odd
14523
+ # revision fails safe to the established refresh path; a failed derived pass is NOT memoised.
14524
+ # The in-process scheduler and the external tick can arrive together. Recheck the marker
14525
+ # *inside* one tenant lock so they cannot both pay for the same full document before either
14526
+ # has memoised it. The lock covers only the derived tail; schedules above remain independent.
14527
+ with _derived_tick_lock(tenant):
14528
+ _derived_due, _derived_marker = _derived_refresh_due(rt, tenant, now=now)
14529
+ if _derived_due:
14530
+ _derived_ok = True
14531
+ # C7's amendment: date-window metrics advance with the tick, so `today` is never staler
14532
+ # than one tick while a scheduler exists. A tenant without metric fields pays a dict scan.
14533
+ try:
14534
+ refresh_metrics(rt, log=log)
14535
+ except Exception as e: # noqa: BLE001
14536
+ _derived_ok = False
14537
+ log(f"[aios-auto] metric refresh {tenant} failed: {type(e).__name__}: {e}")
14538
+ # ⭐ 2026-08-07 β€” the relational pass, on the same tick and for the same reason. A rollup
14539
+ # on table A goes stale when table B gains a row, and A cannot know that happened; the
14540
+ # tick is the only place that sees both. Separate failure handling is retained.
14541
+ try:
14542
+ refresh_relations(rt, log=log)
14543
+ except Exception as e: # noqa: BLE001
14544
+ _derived_ok = False
14545
+ log(f"[aios-auto] relation refresh {tenant} failed: {type(e).__name__}: {e}")
14546
+ # Source-backed rollups move with Odoo and with their own date windows, so they share the
14547
+ # same durable-revision/day gate but retain their independent failure posture.
14548
+ try:
14549
+ _refresh_source_rollups(rt, tenant, log=log)
14550
+ except Exception as e: # noqa: BLE001
14551
+ _derived_ok = False
14552
+ log(f"[aios-auto] source rollup {tenant} failed: {type(e).__name__}: {e}")
14553
+ if _derived_ok and _derived_marker is not None:
14554
+ _DERIVED_TICK_STATE[str(tenant)] = _derived_marker
14555
  if started:
14556
  log(f"[aios-auto] tick {tenant}: started {', '.join(started)}")
14557
  return started
14558
 
14559
 
14560
+ #: Postgres-only memo for the expensive derived half of `tick`. The key is the tenant slug; the
14561
+ #: marker contains both the durable bucket revision and the UTC calendar date. It is process
14562
+ #: memory by design: a new container pays one correct refresh, then resumes the no-idle-egress
14563
+ #: posture. Do not use it for schedules β€” those stay on the minute loop above.
14564
+ _DERIVED_TICK_STATE = {}
14565
+ _DERIVED_TICK_LOCKS = {}
14566
+ _DERIVED_TICK_LOCKS_GUARD = threading.Lock()
14567
+
14568
+
14569
+ def _derived_tick_lock(tenant):
14570
+ """The one derived-work lock for this process and tenant, created without a map race."""
14571
+ key = str(tenant)
14572
+ with _DERIVED_TICK_LOCKS_GUARD:
14573
+ return _DERIVED_TICK_LOCKS.setdefault(key, threading.Lock())
14574
+
14575
+
14576
+ def _derived_refresh_due(rt, tenant, now=None, state=None, backend=None):
14577
+ """Return `(due, marker)` for full-document derived work on a scheduler tick.
14578
+
14579
+ `marker is None` means no safe durable observation exists, so callers must preserve the
14580
+ previous behaviour. The helper is side-effect free; `tick` records a marker only after all
14581
+ three isolated passes completed, making a transient failure retry on the next minute.
14582
+ """
14583
+ actual_backend = backend if backend is not None else os.environ.get("STORE_BACKEND", "hf")
14584
+ if str(actual_backend).strip().lower() != "pg":
14585
+ return True, None
14586
+ try:
14587
+ rev = (rt.revision(UT_STORE_KEY) or {}).get("token")
14588
+ if not rev:
14589
+ return True, None
14590
+ except Exception:
14591
+ return True, None
14592
+ # The deployed workers need one calendar, not their host-local timezone. Naive test times
14593
+ # represent UTC; an aware injected time is normalised before choosing the daily window.
14594
+ stamp = now or _dt.datetime.now(_dt.timezone.utc)
14595
+ if isinstance(stamp, _dt.datetime):
14596
+ if stamp.tzinfo is None:
14597
+ stamp = stamp.replace(tzinfo=_dt.timezone.utc)
14598
+ else:
14599
+ stamp = stamp.astimezone(_dt.timezone.utc)
14600
+ day = stamp.date().isoformat() if hasattr(stamp, "date") else str(stamp)[:10]
14601
+ marker = (str(rev), day)
14602
+ memo = _DERIVED_TICK_STATE if state is None else state
14603
+ return memo.get(str(tenant)) != marker, marker
14604
+
14605
+
14606
+ def _refresh_source_rollups(rt, tenant, log=print):
14607
+ """Refresh every source-backed rollup; callers provide the no-op policy.
14608
+
14609
+ The existing per-pass error boundary lives in :func:`tick`. Keeping this helper focused on
14610
+ the actual full-document work makes the Postgres revision/day policy testable without a
14611
+ schedule, vendor call, or a hidden duplicate of the loop.
14612
+ """
14613
+ import rollup_sql
14614
+ for _tk in list((rt.get(UT_STORE_KEY) or {})):
14615
+ if rollup_sql.source_fields((rt.get(UT_STORE_KEY) or {}).get(_tk) or {}):
14616
+ # `today` is DEFAULTED INSIDE `compute`, matching `refresh_metrics` above.
14617
+ n = rollup_sql.compute(rt, _tk)
14618
+ if n:
14619
+ log(f"[aios-auto] source rollups {tenant}/{_tk}: {n}")
14620
+
14621
+
14622
  def _pg_tick_refusal(tenant_slug, backend=None, write_refusal=None):
14623
  """The Pg write wall, before a background tick reads a tenant's whole workspace.
14624
 
api/rollup_sql.py CHANGED
@@ -126,7 +126,14 @@ def _today():
126
 
127
 
128
  def compute(rt, table_key, today=None, tables=None):
129
- """Write every source-backed rollup cell on `table_key`. Returns `{field_key: cells_written}`.
 
 
 
 
 
 
 
130
 
131
  `tables` (a live `user_tables` dict) is the gate's injection point β€” the same shape
132
  `automation_engine.compute_relation_cells` takes, so this can be proven against a fixture
@@ -167,13 +174,23 @@ def compute(rt, table_key, today=None, tables=None):
167
  hit = values.get(join)
168
  if hit is None and join.endswith(".0"):
169
  hit = values.get(join[:-2])
170
- row[fkey] = "" if hit is None else str(hit)
171
- n += 1
 
 
172
  written[fkey] = n
173
  return cur
174
 
175
  if owned:
176
  _apply(blob)
177
- else:
178
- rt.update(ut.STORE_KEY, _apply, flush="async")
179
- return written
 
 
 
 
 
 
 
 
 
126
 
127
 
128
  def compute(rt, table_key, today=None, tables=None):
129
+ """Write changed source-backed rollup cells on `table_key`.
130
+
131
+ Returns ``{field_key: cells_changed}``. A store-backed call first applies the already-resolved
132
+ plans to its read snapshot. If no cell would change it deliberately never enters ``update``:
133
+ Pg's read-modify-write increments the document revision even when its JSON payload is byte-for-
134
+ byte equal, which would make an unchanged scheduler tick look like new source data forever.
135
+ When cells do differ, the plans are applied again inside the store's normal locked update so a
136
+ concurrent row edit is preserved and is picked up by the next revision-driven pass.
137
 
138
  `tables` (a live `user_tables` dict) is the gate's injection point β€” the same shape
139
  `automation_engine.compute_relation_cells` takes, so this can be proven against a fixture
 
174
  hit = values.get(join)
175
  if hit is None and join.endswith(".0"):
176
  hit = values.get(join[:-2])
177
+ value = "" if hit is None else str(hit)
178
+ if row.get(fkey) != value:
179
+ row[fkey] = value
180
+ n += 1
181
  written[fkey] = n
182
  return cur
183
 
184
  if owned:
185
  _apply(blob)
186
+ return {k: n for k, n in written.items() if n}
187
+
188
+ # A snapshot preflight costs one full read only on a due source-rollup pass. It avoids the far
189
+ # more expensive outcome on an idle tenant: a locked JSONB RMW, revision bump and then another
190
+ # full derived pass every minute because the scheduler can never observe a stable revision.
191
+ _apply(blob)
192
+ if not any(written.values()):
193
+ return {}
194
+ written.clear()
195
+ rt.update(ut.STORE_KEY, _apply, flush="async")
196
+ return {k: n for k, n in written.items() if n}
api/routes_changes.py CHANGED
@@ -182,7 +182,12 @@ def _may_watch(session, scope):
182
  hit = _WALL_CACHE.get(key)
183
  if hit and hit[0] > now:
184
  return hit[1]
185
- verdict = bool(user_tables.may_open(scope, session.uname, session.admin, st=session.runtime))
 
 
 
 
 
186
  # β›” ONLY A `True` IS CACHED, and the asymmetry is deliberate. `user_tables.all_tables()`
187
  # SWALLOWS a store error and answers `{}`, which makes `may_open` False β€” so caching a
188
  # negative would turn one transient store hiccup into "no database <name> here" for the whole
 
182
  hit = _WALL_CACHE.get(key)
183
  if hit and hit[0] > now:
184
  return hit[1]
185
+ # `may_open` reads only a table definition (and, for a non-owner, the small shares bucket).
186
+ # Lend it the existing rows-free projection rather than a raw runtime: a cache miss remains
187
+ # the SAME ACL resolver and the same 30 s revocation bound, but Pg never sends the 28.6 MB
188
+ # `rows` payload just to decide whether this signed-in tab may watch its open database.
189
+ verdict = bool(user_tables.may_open(
190
+ scope, session.uname, session.admin, st=user_tables.lend_defs(session.runtime)))
191
  # β›” ONLY A `True` IS CACHED, and the asymmetry is deliberate. `user_tables.all_tables()`
192
  # SWALLOWS a store error and answers `{}`, which makes `may_open` False β€” so caching a
193
  # negative would turn one transient store hiccup into "no database <name> here" for the whole
platform/harness/runtime.py CHANGED
@@ -179,6 +179,16 @@ class TenantRuntime:
179
  """
180
  return self._store().get_projection(self.store_key(name), drop=drop)
181
 
 
 
 
 
 
 
 
 
 
 
182
  def put(self, name, data):
183
  return self._store().put(self.store_key(name), data)
184
 
 
179
  """
180
  return self._store().get_projection(self.store_key(name), drop=drop)
181
 
182
+ def revision(self, name):
183
+ """The tiny durable change token for this tenant's addressed bucket.
184
+
185
+ `get_projection` is for a rows-free document; this is deliberately smaller still. It
186
+ delegates through the same bound store and `store_key()` seam as every other operation so
187
+ a shared-repository tenant asks for ``t/<tenant>/user_tables`` rather than tenant #0's
188
+ bucket. The Pg implementation reads only ``rev, updated_at`` from ``store_kv``.
189
+ """
190
+ return self._store().revision(self.store_key(name))
191
+
192
  def put(self, name, data):
193
  return self._store().put(self.store_key(name), data)
194