toga-ai 1.0.823 → 1.0.824

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -6,8 +6,8 @@ project: Tools
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-09-04
10
- owners: [ajean]
9
+ updated: 2026-09-16
10
+ owners: [ajean, bala]
11
11
  files:
12
12
  - tools/_/app/compass.php
13
13
  - tools/mvc/compass/users/get.php
@@ -101,6 +101,10 @@ conditions of the decision below — **do not remove one without re-opening it**
101
101
  (the same human on two `Users` rows).
102
102
  5. The four `ensureCrossRegionUsersActive()` addresses are **blocked as both source and target**
103
103
  (`isCrossRegionProtectedEmail()`), because that hardcoded worker2 list matches on email.
104
+ **⚠ Out of date as of 2026-09-16:** worker2 split that list into six addresses
105
+ (`ALWAYS_ACTIVE_EMAILS_BOTH` + `ALWAYS_ACTIVE_EMAILS_CA`), so the two new
106
+ `@compassdigital.io` Canada-protected addresses are **not** blocked here yet. See
107
+ [PEOPLE-File User Lifecycle](../../../clients/compass-usa/features/people-file-user-lifecycle.md).
104
108
  6. The **audit row is written before the write**, on its own connection (see the transaction
105
109
  gotcha below).
106
110
 
@@ -191,9 +195,14 @@ names, reads `Core.ClientEmailDomains` / `Core.Clients`, and inserts into `Logs_
191
195
  ~178,615 of 178,616 `Client_Compass` rows, so the client id cannot be derived from the user row.
192
196
  See [Compass Canada profile](../../../clients/compass-canada/profile.md).
193
197
  - **A cross-region user still needs a code change and a deploy.** The only durable way to hold one
194
- active is the hardcoded four-address list in `ensureCrossRegionUsersActive()` in
198
+ active is the hardcoded list in `ensureCrossRegionUsersActive()` in
195
199
  `worker2/Worker/Client/Compass/PeopleFile.php`. This tool does not change that — and because that
196
- list matches on **email**, those four addresses are blocked from this tool's email edit entirely.
200
+ list matches on **email**, those addresses are blocked from this tool's email edit entirely.
201
+ **As of 2026-09-16 that list is six addresses, not four**: `ALWAYS_ACTIVE_EMAILS_BOTH` (the
202
+ original four, held active in both tenants) plus `ALWAYS_ACTIVE_EMAILS_CA` (two
203
+ `@compassdigital.io` US staff held active in `Client_CompassCanada` only).
204
+ `isCrossRegionProtectedEmail()` here still only knows the original four, so the two new addresses
205
+ can be edited in this tool and silently lose their protection.
197
206
  - **Every user search is a full table scan.** `Client_Compass.Users` has no index on `email`,
198
207
  `c_hrEmpUsername`, `c_hrEmpPersonnelNbr` or `isActive` (176k rows; `EXPLAIN` reports `type=ALL`,
199
208
  no possible keys). Same on `Client_CompassCanada`. Keep result sets bounded and do not add
@@ -235,12 +244,22 @@ names, reads `Core.ClientEmailDomains` / `Core.Clients`, and inserts into `Logs_
235
244
  (`_underscore/Model/Compass/SalesOrder.php:287-305`): it joins
236
245
  `INNER JOIN Contacts ON Contacts.id = Users.id`, which should be `Users.contactId`.
237
246
  - **worker2** — scope `ensureCrossRegionUsersActive()` to a `Users.id` list instead of matching on
238
- email; that would also lift this tool's block on those four addresses.
247
+ email; that would also lift this tool's block on those addresses.
248
+ - **tools** — extend `isCrossRegionProtectedEmail()` to the two `ALWAYS_ACTIVE_EMAILS_CA` addresses
249
+ added in worker2 on 2026-09-16, or the block silently under-covers the protected set.
239
250
  - **ops** — rotate every secret in `tools/config.production.ini`; consider a least-privilege
240
251
  Compass account.
241
252
 
242
253
  ## Change history
243
254
 
255
+ - 2026-09-16 — Recorded that worker2's cross-region protected list **grew from four addresses to
256
+ six** (`ALWAYS_ACTIVE_EMAILS_BOTH` + `ALWAYS_ACTIVE_EMAILS_CA`), so this tool's
257
+ `isCrossRegionProtectedEmail()` block now under-covers it: the two new `@compassdigital.io`
258
+ Canada-protected addresses can still be email-edited here and would silently drop out of the
259
+ protected set, because the worker2 match is on email. No `tools` code changed. Detail:
260
+ [PEOPLE-File User Lifecycle](../../../clients/compass-usa/features/people-file-user-lifecycle.md).
261
+ (bala)
262
+
244
263
  - 2026-09-04 — Added **email, phone and cost-center editing** (PR tools#14,
245
264
  `feature/compass-user-field-edits`, **open / not merged / not deployed**): new `App_Compass`
246
265
  setters plus the `normaliseEmail` / `normalisePhone` / `emailDomainIsRegistered` /
@@ -6,7 +6,7 @@ project: Worker
6
6
  client: compass-usa
7
7
  type: client-feature
8
8
  status: active
9
- updated: 2026-09-04
9
+ updated: 2026-09-16
10
10
  owners: [bala, ajean]
11
11
  files:
12
12
  - worker2/Worker/Client/Compass/PeopleFile.php
@@ -85,6 +85,39 @@ insert-refresh time the **new row already has its id** and the **old row is stil
85
85
  deactivation-time hook would be worse — by then the new row's relationship to the old one has to be
86
86
  rediscovered, and the deactivation path has no model hooks at all (see below).
87
87
 
88
+ ### Always-active protection is per-tenant, and has two layers
89
+
90
+ A small hardcoded set of people must stay active regardless of what HR sends. Since **2026-09-16**
91
+ that set is split into two class constants on `_Worker_Client_Compass_PeopleFile`:
92
+
93
+ - **`ALWAYS_ACTIVE_EMAILS_BOTH`** — the original four addresses (two `@compass-usa.com`, two
94
+ `@compass-canada.com`). Held active in **both** `Client_Compass` and `Client_CompassCanada`.
95
+ - **`ALWAYS_ACTIVE_EMAILS_CA`** — two `@compassdigital.io` **Compass USA** staff (usernames
96
+ `shuttk01` / `racerk01` in `Client_Compass`). Held active in **`Client_CompassCanada` only**.
97
+ They are allowed to go inactive in the US tenant when the PEOPLE file drops them; that is the
98
+ requirement, not a bug.
99
+
100
+ `isEmailAlwaysActive(string $email, string $tenantKey): bool` is the single decision point — a
101
+ lowercased match that returns true for the BOTH list in either tenant, and for the CA list only when
102
+ `$tenantKey === 'CA'`. Two layers consume it:
103
+
104
+ 1. **The inactivation loop** in `executeForTenant()` skips any user whose `Users.email` is
105
+ protected for that tenant, so the batch `UPDATE ... SET isActive = 0` never lists them.
106
+ 2. **`ensureCrossRegionUsersActive()`**, which runs after both tenants have executed and force-sets
107
+ `isActive = 1` — in both databases for the BOTH list, and in `Client_CompassCanada` only for the
108
+ CA list.
109
+
110
+ **Today only layer 2 actually protects the CA-only pair.** In `Client_CompassCanada` both of those
111
+ users have `c_hrEmpUsername = NULL`, and `loadLookups()` keys its map on username — so they never
112
+ enter the map and can never reach the inactivation batch on the Canada side. The loop guard is the
113
+ defence for the day HR gives them a Canada username, not what is holding them active now.
114
+ (Read-only prod check, 2026-09-16.)
115
+
116
+ **The email match works across a case difference.** `Client_Compass` stores these two addresses
117
+ UPPERCASE while `Client_CompassCanada` stores them lowercase. `Users.email` uses
118
+ `utf8mb4_0900_ai_ci`, a case-insensitive collation, so `WHERE email = '...'` still hits — each
119
+ address resolves to exactly **one** row per database.
120
+
88
121
  ## Gotchas
89
122
 
90
123
  - **⚠ Deactivation is raw bulk SQL — model hooks never fire.** User inactivation is executed as
@@ -157,16 +190,26 @@ rediscovered, and the deactivation path has no model hooks at all (see below).
157
190
  makes the [Tools Compass admin
158
191
  pages](../../../1.0/apps/tools/features/compass-user-persona-admin.md) a one-way-durable tool.
159
192
  - **`ensureCrossRegionUsersActive()` runs `UPDATE Users SET isActive = 1 WHERE email = X` with no
160
- `isActive` filter**, so it reactivates **every** row carrying one of its four hardcoded addresses,
161
- dead duplicates included. Verified on prod 2026-09-02: each of the four addresses currently
162
- resolves to exactly **one** active row in **both** tenants (zero inactive), so nothing is being
163
- resurrected today — but the query would happily do it if a duplicate appeared. That hardcoded list
164
- is also still the **only** durable way to hold a cross-region user active, and changing it needs a
165
- code change and a deploy per request. **Recommended (not done): scope it to a `Users.id` list
166
- instead of matching on email** — matching on email means an email edit silently moves a user in or
167
- out of the protected set, which is why the [Tools Compass
168
- admin](../../../1.0/apps/tools/features/compass-user-persona-admin.md) blocks those four addresses
169
- from being edited at all.
193
+ `isActive` filter**, so it reactivates **every** row carrying a protected address, dead duplicates
194
+ included. Verified on prod 2026-09-02 for the original four, and 2026-09-16 for the two CA-only
195
+ addresses: each resolves to exactly **one** row per database, so nothing is being resurrected
196
+ today — but the query would happily do it if a duplicate appeared. That hardcoded list is also
197
+ still the **only** durable way to hold a cross-region user active, and changing it needs a code
198
+ change and a deploy per request. **Recommended (not done): scope it to a `Users.id` list instead
199
+ of matching on email** — matching on email means an email edit silently moves a user in or out of
200
+ the protected set, which is why the [Tools Compass
201
+ admin](../../../1.0/apps/tools/features/compass-user-persona-admin.md) blocks the protected
202
+ addresses from being edited at all.
203
+ - **⚠ The Tools admin's edit block covers only the original four addresses, not the two CA-only
204
+ ones.** The protected set grew to six in worker2 on 2026-09-16; the `tools` block list was not
205
+ changed and was not re-checked. Until it is, someone can edit the email of a CA-protected user in
206
+ [Tools Compass admin](../../../1.0/apps/tools/features/compass-user-persona-admin.md) and silently
207
+ drop them out of the protected set — because the match is on email, not id.
208
+ - **⚠ Adding a new "always keep this person active" rule means touching exactly two places.**
209
+ `executeForTenant()`'s inactivation loop is the only `isActive = 0` writer in the whole class, and
210
+ `ensureCrossRegionUsersActive()` is the post-run sweep. Guard both and the requirement is covered;
211
+ guard only the loop and the post-run sweep still decides the final state, guard only the sweep and
212
+ the user flickers inactive between the two steps.
170
213
 
171
214
  ## Remediation — BUILT 2026-08-20
172
215
 
@@ -197,6 +240,24 @@ question is now contained rather than blocking.
197
240
 
198
241
  ## Change history
199
242
 
243
+ - 2026-09-16 — **Made the always-active protection tenant-aware.** Two Compass USA staff
244
+ (`@compassdigital.io`, usernames `shuttk01` / `racerk01`) exist in **both** tenants, but the
245
+ PEOPLE file lists them only as US rows (`c_erpSystemId` 1001), so they were routed to
246
+ `Client_Compass` alone and had no protection on the Canada side. Requirement: they may go inactive
247
+ in the USA per the file, but must **always** stay active in Canada. Split the previously
248
+ undocumented single hardcoded list into `ALWAYS_ACTIVE_EMAILS_BOTH` (the original four) and
249
+ `ALWAYS_ACTIVE_EMAILS_CA` (the two new ones), added
250
+ `isEmailAlwaysActive(string $email, string $tenantKey): bool` as the single lowercased decision
251
+ point, and wired it into **both** the `executeForTenant()` inactivation loop (skip protected
252
+ users) and `ensureCrossRegionUsersActive()` (both DBs for BOTH, `Client_CompassCanada` only for
253
+ CA). Prod read-only findings: in `Client_CompassCanada` both users have `c_hrEmpUsername = NULL`,
254
+ so they never enter `loadLookups()`'s username-keyed map and **cannot** reach the Canada
255
+ inactivation batch at all — the post-run sweep is what actually protects them today, and the loop
256
+ guard is defence for the day they get a Canada username. Also confirmed the two tenants store
257
+ these addresses in different case (US uppercase, CA lowercase) and the email match still works
258
+ because `Users.email` uses the case-insensitive `utf8mb4_0900_ai_ci` collation, one row per
259
+ address per database. Not committed or pushed — worker2 working tree only. (bala)
260
+
200
261
  - 2026-09-04 — Prod-verified the **full index list** on `Users` in both tenants and recorded the
201
262
  load-bearing consequence: **there is no `UNIQUE KEY` on `email`** (and no index on
202
263
  `c_hrEmpUsername` / `c_hrEmpPersonnelNbr` / `isActive`), so the database silently accepts duplicate
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.823",
3
+ "version": "1.0.824",
4
4
  "description": "TOGA Technology Team Claude Knowledge System — shared AI coding harness with skills, knowledge base CLI, and project installer for Claude Code.",
5
5
  "keywords": [
6
6
  "claude",