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

Deploy AIOS web (React glide grid + FastAPI slice)

Browse files
RELEASES.json CHANGED
@@ -1,6 +1,12 @@
1
  {
2
- "current": "v34 (cb4caa1)",
3
  "releases": [
 
 
 
 
 
 
4
  {
5
  "version": "v34",
6
  "sha": "cb4caa1",
 
1
  {
2
+ "current": "v35 (0f64212)",
3
  "releases": [
4
+ {
5
+ "version": "v35",
6
+ "sha": "0f64212",
7
+ "date": "2026-08-21",
8
+ "subject": "W39 visual QA fixture"
9
+ },
10
  {
11
  "version": "v34",
12
  "sha": "cb4caa1",
VERSION CHANGED
@@ -1 +1 @@
1
- v34 (cb4caa1)
 
1
+ v35 (0f64212)
api/deps.py CHANGED
@@ -515,7 +515,7 @@ def _user_for(claims):
515
  if not uname:
516
  return None
517
  claim_tenant = str(claims.get("t") or "").strip().lower()
518
- # W39-T28 / A62. This MUST precede the registry read: qa-runner is specifically a
519
  # zero-touch qa-b identity, not an account in tenant #0's `users` document. The helper
520
  # rechecks both non-production deployment flags, so removing either invalidates its cookie.
521
  qa = users.qa_identity(uname)
@@ -656,9 +656,10 @@ def require_session(request: Request, response: Response) -> Session:
656
  response.headers[_TENANT_HEADER] = str(claims["t"])
657
  # R4: last AFTER the session is fully resolved β€” a stamp is only true of a request that was
658
  # actually admitted, and putting it here means no failed-auth path can ever write one.
659
- # qa-runner has no global account record. Never let the normal activity telemetry create or
660
- # touch one through the default tenant-#0 users store.
661
- if not user.get("qa_runner"):
 
662
  _touch_active(user.get("username"))
663
  return Session(tenant=claims["t"], user=user, claims=claims, runtime=rt)
664
 
 
515
  if not uname:
516
  return None
517
  claim_tenant = str(claims.get("t") or "").strip().lower()
518
+ # W39-T28 / A62. This MUST precede the registry read: a synthetic QA principal is a
519
  # zero-touch qa-b identity, not an account in tenant #0's `users` document. The helper
520
  # rechecks both non-production deployment flags, so removing either invalidates its cookie.
521
  qa = users.qa_identity(uname)
 
656
  response.headers[_TENANT_HEADER] = str(claims["t"])
657
  # R4: last AFTER the session is fully resolved β€” a stamp is only true of a request that was
658
  # actually admitted, and putting it here means no failed-auth path can ever write one.
659
+ # Synthetic QA identities have no global account record. Never let normal activity telemetry
660
+ # create or touch one through the default tenant-#0 users store (the visual fixture has two
661
+ # Members, so this must not be specific to qa-runner).
662
+ if not user.get("qa_fixture"):
663
  _touch_active(user.get("username"))
664
  return Session(tenant=claims["t"], user=user, claims=claims, runtime=rt)
665
 
api/routes_auth.py CHANGED
@@ -187,15 +187,16 @@ def login(request: Request, response: Response, body: dict = Body(default=None))
187
  # stamping what was typed would write onto a key that does not exist. Fail-silent inside
188
  # `touch_login` and a no-op for the emergency-master identity (which has no record to stamp),
189
  # so the one thing this cannot do is turn a good credential into a failed sign-in.
190
- # The synthetic QA identity has no normal account record to stamp. More importantly, a
191
  # registry stamp here would default to tenant #0 and make a qa-b proof touch the wrong schema.
192
- if not user.get("qa_runner"):
 
193
  users.touch_login(user["username"])
194
  # `touch_login` also sets `last_active`, so tell the session resolver it has been seen β€”
195
  # otherwise the very next request stamps it again and the hourly throttle is off by one write
196
  # per sign-in.
197
  import deps as _deps
198
- if not user.get("qa_runner"):
199
  _deps.note_active(user["username"])
200
  return {"user": _public_user(user)}
201
 
 
187
  # stamping what was typed would write onto a key that does not exist. Fail-silent inside
188
  # `touch_login` and a no-op for the emergency-master identity (which has no record to stamp),
189
  # so the one thing this cannot do is turn a good credential into a failed sign-in.
190
+ # Synthetic QA identities have no normal account record to stamp. More importantly, a
191
  # registry stamp here would default to tenant #0 and make a qa-b proof touch the wrong schema.
192
+ # This is ``qa_fixture`` rather than ``qa_runner`` because visual sharing needs two Members.
193
+ if not user.get("qa_fixture"):
194
  users.touch_login(user["username"])
195
  # `touch_login` also sets `last_active`, so tell the session resolver it has been seen β€”
196
  # otherwise the very next request stamps it again and the hourly throttle is off by one write
197
  # per sign-in.
198
  import deps as _deps
199
+ if not user.get("qa_fixture"):
200
  _deps.note_active(user["username"])
201
  return {"user": _public_user(user)}
202
 
api/routes_customers.py CHANGED
@@ -38,6 +38,31 @@ MODULE = "customer_data"
38
  _CACHE_TTL = 900 # the pool build is slow (Odoo + reconciliation); 15 min, as before
39
 
40
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
41
  def _pool_rows(session: Session):
42
  """The reconciled Odoo pool for this session's SCOPE β€” the slow part, and the only part that
43
  may be shared between users.
@@ -59,6 +84,9 @@ def _pool_rows(session: Session):
59
  `(team_id, agent)` pair, so no two of them ever collided on the key β€” the test set could not
60
  express the bug. `verify_api.py` now carries a same-scope second user for exactly this.
61
  """
 
 
 
62
  rt = session.runtime
63
  team_id, agent = _team_agent(session)
64
  return _pool_for(rt, team_id, agent)
@@ -245,7 +273,8 @@ def grid_assembly(session: Session, scope: str = "customer", storage_key: str =
245
 
246
  rt = session.runtime
247
  team_id, agent = _team_agent(session)
248
- rows_src = _pool_for(rt, team_id, agent)
 
249
  # β›” THE ROW WALL, APPLIED BEFORE `pids` IS TAKEN. Everything downstream is bounded by that
250
  # frozenset β€” `allowed_pids` for the workspace, cohort membership, measure resolution β€” so
251
  # scoping here means a row this account may not see never enters ANY of them, rather than
@@ -333,7 +362,11 @@ def grid_assembly(session: Session, scope: str = "customer", storage_key: str =
333
  # picker and refuse new creates while an EXISTING Metric column kept computing and an
334
  # EXISTING measure condition kept resolving β€” a revoked capability still answering, on the
335
  # grain with the most of them. Three calls, one predicate.
336
- may_metrics = perm_scope.may_metrics(session.user, MODULE)
 
 
 
 
337
  measures = measure_resolve.offer(team_id, on_error=_measure_err) if may_metrics else []
338
  measure_sets = measure_resolve.condition_sets(
339
  [v.get("config") or {} for v in (views or [])], None, team_id, pids, today, stamp,
 
38
  _CACHE_TTL = 900 # the pool build is slow (Odoo + reconciliation); 15 min, as before
39
 
40
 
41
+ # W39 visual QA fixture. The staging-only QA tenant has an intentionally empty connector, so it
42
+ # can prove tenant isolation; a browser cannot, however, exercise Map routing or a third-row Go
43
+ # link with zero rows. These static, non-customer coordinates are the narrowest harmless fixture:
44
+ # three local map points, no Odoo source access, and no path for a normal tenant to receive them.
45
+ _QA_CUSTOMER_ROWS = (
46
+ {"pid": 9_900_001, "name": "QA Route North", "dba": "QA", "agent": "QA",
47
+ "_created": "2026-08-21", "lat": 41.8884, "lon": -87.6354,
48
+ "revenue_ytd": 0.0, "orders_24m": 0, "balance": 0.0, "overdue": 0.0, "ar_open": 0.0},
49
+ {"pid": 9_900_002, "name": "QA Route Centre", "dba": "QA", "agent": "QA",
50
+ "_created": "2026-08-21", "lat": 41.8781, "lon": -87.6298,
51
+ "revenue_ytd": 0.0, "orders_24m": 0, "balance": 0.0, "overdue": 0.0, "ar_open": 0.0},
52
+ {"pid": 9_900_003, "name": "QA Route South", "dba": "QA", "agent": "QA",
53
+ "_created": "2026-08-21", "lat": 41.8677, "lon": -87.6241,
54
+ "revenue_ytd": 0.0, "orders_24m": 0, "balance": 0.0, "overdue": 0.0, "ar_open": 0.0},
55
+ )
56
+
57
+
58
+ def _qa_customer_rows(session: Session):
59
+ """A detached three-row fixture for the admitted non-production QA identities only."""
60
+ if (session.tenant == "qa-b" and isinstance(session.user, dict)
61
+ and session.user.get("qa_fixture") is True):
62
+ return [dict(row) for row in _QA_CUSTOMER_ROWS]
63
+ return None
64
+
65
+
66
  def _pool_rows(session: Session):
67
  """The reconciled Odoo pool for this session's SCOPE β€” the slow part, and the only part that
68
  may be shared between users.
 
84
  `(team_id, agent)` pair, so no two of them ever collided on the key β€” the test set could not
85
  express the bug. `verify_api.py` now carries a same-scope second user for exactly this.
86
  """
87
+ qa_rows = _qa_customer_rows(session)
88
+ if qa_rows is not None:
89
+ return qa_rows
90
  rt = session.runtime
91
  team_id, agent = _team_agent(session)
92
  return _pool_for(rt, team_id, agent)
 
273
 
274
  rt = session.runtime
275
  team_id, agent = _team_agent(session)
276
+ qa_rows = _qa_customer_rows(session)
277
+ rows_src = qa_rows if qa_rows is not None else _pool_for(rt, team_id, agent)
278
  # β›” THE ROW WALL, APPLIED BEFORE `pids` IS TAKEN. Everything downstream is bounded by that
279
  # frozenset β€” `allowed_pids` for the workspace, cohort membership, measure resolution β€” so
280
  # scoping here means a row this account may not see never enters ANY of them, rather than
 
362
  # picker and refuse new creates while an EXISTING Metric column kept computing and an
363
  # EXISTING measure condition kept resolving β€” a revoked capability still answering, on the
364
  # grain with the most of them. Three calls, one predicate.
365
+ # A QA fixture must not turn a browser proof into an Odoo measurement request. The synthetic
366
+ # rows intentionally exercise grid/map/navigation only; metrics have no fixture truth and are
367
+ # therefore unavailable rather than silently reaching a real source.
368
+ may_metrics = (not bool(session.user.get("qa_fixture"))
369
+ and perm_scope.may_metrics(session.user, MODULE))
370
  measures = measure_resolve.offer(team_id, on_error=_measure_err) if may_metrics else []
371
  measure_sets = measure_resolve.condition_sets(
372
  [v.get("config") or {} for v in (views or [])], None, team_id, pids, today, stamp,
api/routes_shares.py CHANGED
@@ -282,6 +282,13 @@ def get_share(kind: str, oid: str, session: Session = Depends(require_session)):
282
  def _people(tenant):
283
  """[{username, name}] for this tenant β€” same population as `assignable_people`, with the
284
  BINDING identity alongside the display one."""
 
 
 
 
 
 
 
285
  try:
286
  reg = users.registry() or {}
287
  except Exception:
 
282
  def _people(tenant):
283
  """[{username, name}] for this tenant β€” same population as `assignable_people`, with the
284
  BINDING identity alongside the display one."""
285
+ # W39 visual QA: qa-b is an explicitly enabled, synthetic two-Member fixture. Its roster
286
+ # must resolve before the normal registry path: that registry is tenant #0 on Postgres, so
287
+ # even a read-only Share dialog would otherwise cross the isolation boundary the fixture is
288
+ # intended to prove. ``None`` preserves the ordinary registry route for every real tenant.
289
+ qa = users.qa_people(tenant)
290
+ if qa is not None:
291
+ return qa
292
  try:
293
  reg = users.registry() or {}
294
  except Exception:
platform/core/users.py CHANGED
@@ -27,12 +27,41 @@ PERMS_VERSION = 1
27
  # identity would make a staging QA check depend on (and potentially mutate) a production-shaped
28
  # account document. The identity exists only on an explicitly-granted non-production build.
29
  _QA_TENANT = 'qa-b'
30
- _QA_USERNAME = 'qa-runner'
31
  _QA_TENANT_GRANT = 'AIOS_SANDBOX_TENANTS'
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
32
 
33
 
34
  def qa_identity(username, pw=None):
35
- """Return the strictly staging-only QA runner, or ``None``.
36
 
37
  Both deployment knobs are required: registration without a schema grant may not authenticate
38
  a writer, and a typed grant without the registration flag may not mint an identity. The
@@ -43,13 +72,8 @@ def qa_identity(username, pw=None):
43
  the same deployment guards still have to hold when it is read back.
44
  """
45
  uname = (username or '').strip().lower()
46
- if uname != _QA_USERNAME:
47
- return None
48
- if os.environ.get('AIOS_ENABLE_QA_TENANT') != '1':
49
- return None
50
- granted = {s.strip().lower() for s in
51
- str(os.environ.get(_QA_TENANT_GRANT) or '').split(',') if s.strip()}
52
- if _QA_TENANT not in granted:
53
  return None
54
  master = os.environ.get('APP_PASSWORD', '')
55
  if not master:
@@ -61,9 +85,9 @@ def qa_identity(username, pw=None):
61
  # exposing the secret; rotating APP_PASSWORD invalidates outstanding QA cookies just as an
62
  # account-record epoch bump would, while still doing zero registry I/O.
63
  qa_epoch = int.from_bytes(hashlib.sha256(master.encode('utf-8')).digest()[:4], 'big')
64
- return {'username': _QA_USERNAME, 'name': 'QA Runner', 'role': 'user',
65
  'bus': 'all', 'modules': ['customers'], 'tenant': _QA_TENANT,
66
- 'epoch': qa_epoch, 'qa_runner': True}
67
 
68
 
69
  def _hash(pw, salt):
 
27
  # identity would make a staging QA check depend on (and potentially mutate) a production-shaped
28
  # account document. The identity exists only on an explicitly-granted non-production build.
29
  _QA_TENANT = 'qa-b'
 
30
  _QA_TENANT_GRANT = 'AIOS_SANDBOX_TENANTS'
31
+ _QA_IDENTITIES = {
32
+ # These principals are deliberately *not* records in ``users``. They exist only to make a
33
+ # Staging browser proof exercise the same Member-to-Member share picker that the product
34
+ # serves, without enumerating tenant #0's account document.
35
+ 'qa-runner': {'name': 'QA Runner', 'qa_runner': True},
36
+ 'qa-member': {'name': 'QA Member', 'qa_runner': False},
37
+ }
38
+
39
+
40
+ def _qa_enabled():
41
+ """Whether the explicit non-production QA fixture is admitted, without registry I/O."""
42
+ if os.environ.get('AIOS_ENABLE_QA_TENANT') != '1':
43
+ return False
44
+ granted = {s.strip().lower() for s in
45
+ str(os.environ.get(_QA_TENANT_GRANT) or '').split(',') if s.strip()}
46
+ return _QA_TENANT in granted
47
+
48
+
49
+ def qa_people(tenant):
50
+ """The non-production-only picker roster, or ``None`` outside the admitted fixture.
51
+
52
+ ``None`` deliberately means "use the ordinary tenant registry"; an empty list would make a
53
+ malformed/disabled configuration look like a real tenant with no accounts. This is kept next
54
+ to ``qa_identity`` so the roster and the principals cannot drift into two admission rules.
55
+ """
56
+ if not _qa_enabled() or str(tenant or '').strip().lower() != _QA_TENANT:
57
+ return None
58
+ return sorted(({"username": uname, "name": spec["name"]}
59
+ for uname, spec in _QA_IDENTITIES.items()),
60
+ key=lambda person: person["name"].lower())
61
 
62
 
63
  def qa_identity(username, pw=None):
64
+ """Return an explicitly admitted Staging QA principal, or ``None``.
65
 
66
  Both deployment knobs are required: registration without a schema grant may not authenticate
67
  a writer, and a typed grant without the registration flag may not mint an identity. The
 
72
  the same deployment guards still have to hold when it is read back.
73
  """
74
  uname = (username or '').strip().lower()
75
+ spec = _QA_IDENTITIES.get(uname)
76
+ if spec is None or not _qa_enabled():
 
 
 
 
 
77
  return None
78
  master = os.environ.get('APP_PASSWORD', '')
79
  if not master:
 
85
  # exposing the secret; rotating APP_PASSWORD invalidates outstanding QA cookies just as an
86
  # account-record epoch bump would, while still doing zero registry I/O.
87
  qa_epoch = int.from_bytes(hashlib.sha256(master.encode('utf-8')).digest()[:4], 'big')
88
+ return {'username': uname, 'name': spec['name'], 'role': 'user',
89
  'bus': 'all', 'modules': ['customers'], 'tenant': _QA_TENANT,
90
+ 'epoch': qa_epoch, 'qa_runner': bool(spec['qa_runner']), 'qa_fixture': True}
91
 
92
 
93
  def _hash(pw, salt):