toga-ai 1.0.574 → 1.0.577

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.
@@ -9,6 +9,7 @@
9
9
  | [ENVIRONMENT (not the EB environment name) decides the _underscore branch and Config file](features/environment-variable-drives-underscore-branch.md) | An api2 Elastic Beanstalk instance decides **which `_underscore` branch it clones** and **which `Config/<env>.ini` it loads** from the EB environment property * | api2/.ebextensions/git.php, api2/.ebextensions/php_include_underscore.config, api2/.ebextensions/git.sandbox-dev.json, api2/Config/beta.ini, api2/Config/sandbox-dev.ini, api2/Component/Api/V2/V2.php |
10
10
  | [Health-check endpoint (/health liveness short-circuit)](features/health-check-endpoint.md) | `_Controller_Index::api()` short-circuits **liveness/health-probe** requests to an HTTP 200 **before** any routing, DB bootstrap, or V2 engine work runs. | api2/Controller/Index.php |
11
11
  | [Language Translation Layer (audience.language + sidecar tables)](features/language-translation-layer.md) | Serves the same TOGa data (Item title/description/longDescription, plus item **feature** text — `Features.name`, `ItemCategoryFeatureGroups.name`, `ItemFeatures | api2/Component/Api/V2/V2.php, api2/Component/Api/V2/Response/Response.php, _underscore/Model/Core/Setting.php, _underscore/Model/Core/RecordField.php, _underscore/Model/Core/DefaultGlobalSetting.php, _underscore/Model/Client/ItemTranslation.php, _underscore/Model/Client/FeatureTranslation.php, _underscore/Model/Client/ItemCategoryFeatureGroupTranslation.php, _underscore/Model/Client/ItemFeatureTranslation.php, dbchanges2/Client/2026-06-23a - ItemTranslations.sql, dbchanges2/Client/2026-06-23b - ItemTranslationsAcl.sql, dbchanges2/Client/2026-07-13a - FeatureTranslations.sql, dbchanges2/Client/2026-07-13b - FeatureTranslationsAcl.sql, dbchanges2/Core/2026-06-23a - RecordFieldsTranslationColumn.sql, dbchanges2/Core/2026-06-23b - ItemTranslationsRecord.sql, dbchanges2/Core/2026-07-13 - FeatureTranslationsRecord.sql |
12
+ | [/auth/login resolves the client from the email domain, not the Bearer token (cross-client user path)](features/login-cross-client-user-resolution.md) | `POST /v2/auth/login` (email/password user login) can silently swap the target client mid-request. | api2/Component/Api/V2/V2.php, _underscore/String.php |
12
13
  | [Nested FK object embedding is gated by the CHILD record's own ACL](features/nested-fk-acl-embedding.md) | When the V2 JSON engine serializes a foreign-key field into a **nested object** (in `getFullModelData()`, ~V2.php L6016-6060), it re-checks the **child** record | api2/Component/Api/V2/V2.php, dbchanges2/Client_Compass/2026-07-23b - PurchaseOrdersRecordReadAcl.sql |
13
14
  | [Nested-relationship writes & child matching (link vs. create)](features/nested-relationship-writes.md) | When a 2.0 API write payload (`POST`/`PUT`) contains a **nested related object** (e.g. | api2/Component/Api/V2/V2.php, _underscore/Model/Client/ContactEmailAddress.php |
14
15
  | [Record Scripts (computed/aggregate /v2 endpoints — the authoring contract)](features/record-scripts.md) | In api2 you almost never write a controller. | api2/Component/Api/V2/V2.php, _underscore/Model/Team/Sprint.php, _underscore/Model/Client/ItemFulfillment.php, _underscore/Query.php |
@@ -0,0 +1,76 @@
1
+ ---
2
+ title: "/auth/login resolves the client from the email domain, not the Bearer token (cross-client user path)"
3
+ framework: "2.0"
4
+ repo: api2
5
+ project: API
6
+ client: shared
7
+ type: feature
8
+ status: active
9
+ updated: 2026-08-13
10
+ owners: [apeterson]
11
+ files:
12
+ - api2/Component/Api/V2/V2.php
13
+ - _underscore/String.php
14
+ related:
15
+ - ./cross-client-data-retrieval.md
16
+ - ./surface-meta-option.md
17
+ - ../../_underscore/features/per-client-database-connections.md
18
+ ---
19
+
20
+ ## What it is
21
+
22
+ `POST /v2/auth/login` (email/password user login) can silently swap the target client
23
+ mid-request. When the credentialed user is a **cross-client (home) user**, the engine
24
+ abandons the client it started on and re-registers the user's **home** client's databases
25
+ before minting the token. Understanding this path is the difference between diagnosing a
26
+ login `EO-1` in minutes vs. hours — especially in local dev, where the home client's DB
27
+ usually does not exist.
28
+
29
+ ## How it works
30
+
31
+ `_Component_Api_V2::execute()` (~L701-744):
32
+
33
+ 1. The user is loaded **by email** from the current client's DB (~L705) — the client is
34
+ resolved from `ClientEmailDomains`, **not** from any Bearer token.
35
+ 2. The submitted password is checked with `_String::passwordVerify()` → `password_verify()`
36
+ (bcrypt) against the local user row (`_underscore/String.php` L563-576).
37
+ 3. **If the password does NOT verify AND the loaded user row has a non-null `clientId` that
38
+ differs from the current client**, the engine treats it as a **cross-client user**: it
39
+ calls `registerClientDatabases($user->clientId)` and re-loads the *home* user via
40
+ `homeClientUserId` (~L716-731). Authentication then completes against the home client.
41
+
42
+ This is the same `homeClientUserId` bridge that
43
+ [cross-client data retrieval](./cross-client-data-retrieval.md) uses to fan a home user out
44
+ across tenant DBs — here it governs which client a login actually authenticates against.
45
+
46
+ ## Gotchas
47
+
48
+ - **A login `EO-1 "Unknown database 'client_<x>'"` is the cross-client branch failing to
49
+ open the home client's DB.** Observed: logging into **Elite** locally with
50
+ `devteam@togatech.com` throws `EO-1 Unknown database 'client_true'`. That account is an
51
+ internal TOGA (client `true` / `Client_True`) user shared into Elite via
52
+ `homeClientUserId`; because its Elite-local password does not verify, the cross-client
53
+ path tries to open `Client_True`, which does not exist in local dev. It works on the dev
54
+ sandbox only because `Client_True` exists there.
55
+ - **Local-dev workaround (never commit):** set the **local** `Client_<Client>` user's own
56
+ bcrypt password hash to a known value so login succeeds on the **direct** path and never
57
+ enters the cross-client branch. Generate with `_String::passwordHash()` /
58
+ `password_hash($pw, PASSWORD_BCRYPT)`. This is a per-machine local fixup, not a code or
59
+ data change to commit.
60
+ - **The api2 dev server talks to `localhost` as `root` — sandbox DB edits are invisible to
61
+ it.** Local stack traces show `mysqli_connect(["localhost","root",...])`. Any ACL/grant/
62
+ password change must be applied to the **localhost** client DB, not a sandbox connection
63
+ in a DB GUI, or the local API will never see it. (In DbGate specifically: "Run"
64
+ autocommits; a query-tab "unsaved" label refers to the unsaved `.sql` *script file*, not
65
+ pending uncommitted data.) This exact confusion — a grant present in sandbox while the API
66
+ read localhost — cost real debugging time.
67
+
68
+ ## Change history
69
+ - 2026-08-13 — Initial capture from an Elite local-dev login debug session (no code
70
+ changes). Documented that `/auth/login` resolves the client from the email domain and, on
71
+ a local-password miss for a user whose `clientId` differs, follows the `homeClientUserId`
72
+ cross-client branch (`V2.php` ~L701-744), which throws `EO-1 Unknown database` when the
73
+ home client's DB is absent locally; plus the local-dev workarounds (set the local bcrypt
74
+ hash; apply changes to the localhost DB, not sandbox). (apeterson)
75
+ </content>
76
+ </invoke>
@@ -6,7 +6,7 @@ project: API
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-07-15
9
+ updated: 2026-08-13
10
10
  owners: [jcardinal, apeterson]
11
11
  files:
12
12
  - api2/Component/Api/V2/V2.php
@@ -94,6 +94,14 @@ FE, the type `RecordSurfaceState` (`src/surface/types.ts`) is
94
94
  silently lack it.
95
95
 
96
96
  ## Change history
97
+ - 2026-08-13 — Confirmed the per-client seed gap in the wild: **Elite** was missing the
98
+ `surfaces/meta` dispatch grant in its client DB, so `GET /v2/surfaces/meta` 403'd EZ-1
99
+ with `identifiers.script = "meta"` while other clients worked. Concrete reference data:
100
+ Core `Records` route `surfaces` id **333** (`aclDatabase = CORE`), Core `RecordScripts`
101
+ `meta` id **28** (GET); the grant lives in `Client_<slug>.AclRecordScripts`
102
+ (`recordScriptId = 28`, `roleId = 1` for CORE Public). This is a per-client seed-gap class
103
+ — the grant must be captured in a `dbchanges2` `Client_<Name>` migration, not just
104
+ inserted locally. (apeterson)
97
105
  - 2026-07-20 — Documented the `meta.surface`/`recordSurfaceState` payload flow: computed only when the
98
106
  `surface` option is present AND action is READ (V2.php ~5688-5717); single-GET shape
99
107
  `{slug, state:{<elementUuid>:{isVisible,isEnabled}}}`, LIST shape
@@ -20,7 +20,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
20
20
 
21
21
  - **_underscore** (_Underscore) _(framework core)_ — 58 doc(s) → [2.0/apps/_underscore/INDEX.md](2.0/apps/_underscore/INDEX.md)
22
22
  - **worker2** (Worker) — 49 doc(s) → [2.0/apps/worker2/INDEX.md](2.0/apps/worker2/INDEX.md)
23
- - **api2** (API) — 23 doc(s) → [2.0/apps/api2/INDEX.md](2.0/apps/api2/INDEX.md)
23
+ - **api2** (API) — 24 doc(s) → [2.0/apps/api2/INDEX.md](2.0/apps/api2/INDEX.md)
24
24
  - **dbchanges2** (Database Changes) _(framework core)_ — 8 doc(s) → [2.0/apps/dbchanges2/INDEX.md](2.0/apps/dbchanges2/INDEX.md)
25
25
  - **toga2-supply** (TOGa Supply) — 6 doc(s) → [2.0/apps/toga2-supply/INDEX.md](2.0/apps/toga2-supply/INDEX.md)
26
26
  - **saml** (SAML SSO Gateway) — 3 doc(s) → [2.0/apps/saml/INDEX.md](2.0/apps/saml/INDEX.md)
@@ -15,7 +15,7 @@ project: Worker
15
15
  client: elite
16
16
  type: profile
17
17
  status: active
18
- updated: 2026-08-12
18
+ updated: 2026-08-13
19
19
  owners: [snaredla, apeterson, tcox, bala]
20
20
  files:
21
21
  - worker2/Worker/Elite.php
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.574",
3
+ "version": "1.0.577",
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",
@@ -68,12 +68,15 @@ function readPayload() {
68
68
  try { data.tool_input = JSON.parse(process.env.CLAUDE_TOOL_INPUT); } catch (e) {}
69
69
  }
70
70
  if (!data.prompt && process.env.CLAUDE_USER_PROMPT) data.prompt = process.env.CLAUDE_USER_PROMPT;
71
- if (!data.session_id && process.env.CLAUDE_SESSION_ID) data.session_id = process.env.CLAUDE_SESSION_ID;
71
+ if (!data.session_id) data.session_id = process.env.CLAUDE_SESSION_ID || process.env.CLAUDE_CODE_SESSION_ID;
72
72
  return data;
73
73
  }
74
74
 
75
75
  function lockPath() {
76
- return path.join(process.cwd(), '.claude', '.kickoff-gate.json');
76
+ // Anchor to the project dir (reliably provided by the harness) rather than the shell's
77
+ // cwd, so the hook and a manually-run `kickoff-primed` always agree on the lock location.
78
+ const base = process.env.CLAUDE_PROJECT_DIR || process.cwd();
79
+ return path.join(base, '.claude', '.kickoff-gate.json');
77
80
  }
78
81
 
79
82
  function readLock() {
@@ -250,10 +253,10 @@ function main() {
250
253
  // preflight ADVANCES to stage 2 (does NOT release) — the team KB is still unprimed.
251
254
  if (isPreflightBash(command)) { writeLock(lock.session, 'primer'); process.exit(0); }
252
255
  if (isPrimingBash(command)) process.exit(0); // other read-only priming step
253
- console.log(armedMessage());
256
+ console.error(armedMessage());
254
257
  process.exit(2);
255
258
  }
256
- if (BLOCKED_TOOLS.has(tool)) { console.log(armedMessage()); process.exit(2); }
259
+ if (BLOCKED_TOOLS.has(tool)) { console.error(armedMessage()); process.exit(2); }
257
260
  process.exit(0); // AskUserQuestion, Skill, TodoWrite, … — part of the interview
258
261
  }
259
262
 
@@ -263,7 +266,7 @@ function main() {
263
266
  // Priming must run in the MAIN session; delegating it (or fanning out other agents
264
267
  // alongside it) is exactly what this stage forbids.
265
268
  if (tool === 'Agent' || tool === 'Task') {
266
- console.log(primerMessage());
269
+ console.error(primerMessage());
267
270
  process.exit(2);
268
271
  }
269
272
  if (tool === 'Bash') {
@@ -271,22 +274,22 @@ function main() {
271
274
  // has read every resolved doc itself, in the main session.
272
275
  if (isPrimedSentinelBash(command)) { clearLock(); process.exit(0); }
273
276
  if (isPrimingBash(command)) process.exit(0); // knowledge.js search/get, version check, …
274
- console.log(primerMessage());
277
+ console.error(primerMessage());
275
278
  process.exit(2);
276
279
  }
277
280
  if (tool === 'Read') {
278
281
  const p = toolInput.file_path || toolInput.path;
279
282
  if (isMemoryReadPath(p)) process.exit(0); // repo-path lookup
280
283
  if (isTeamKbPath(p)) process.exit(0); // reading a team-KB doc IS the priming
281
- console.log(primerMessage()); // repo code — blocked until primed
284
+ console.error(primerMessage()); // repo code — blocked until primed
282
285
  process.exit(2);
283
286
  }
284
287
  if (tool === 'Grep' || tool === 'Glob') {
285
288
  if (isTeamKbSearch(toolInput)) process.exit(0); // locating a doc inside knowledge/
286
- console.log(primerMessage()); // searching repo code — blocked
289
+ console.error(primerMessage()); // searching repo code — blocked
287
290
  process.exit(2);
288
291
  }
289
- if (BLOCKED_TOOLS.has(tool)) { console.log(primerMessage()); process.exit(2); } // Edit/Write/…
292
+ if (BLOCKED_TOOLS.has(tool)) { console.error(primerMessage()); process.exit(2); } // Edit/Write/…
290
293
  process.exit(0); // AskUserQuestion (ask for a repo path), Skill, … — allowed
291
294
  }
292
295