toga-ai 1.0.235 → 1.0.237

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,6 +6,6 @@
6
6
  | [Tools — Developers Folder (UUID & Password Generators)](features/developer-tools.md) | The first two tools shipped in the Tools app, both under the **Developers** folder and gated to personas **Development Team** / **TOGa Technology**. | tools/mvc/developers/uuid/get.php, tools/mvc/developers/password/get.php |
7
7
  | [Tools MVC — Routing, CSRF & App_Database Access Patterns](features/mvc-data-access-patterns.md) | The load-bearing 1.0 (`App_`) framework conventions a developer needs when adding a page to the Tools app — URL routing, CSRF, and DB access through `App_Databa | tools/_/app/nav.php, tools/mvc/get.php |
8
8
  | [Tools Persona-Gated Navigation (App_Nav)](features/persona-gated-navigation.md) | `App_Nav` is the Tools app's two-level, **persona-gated** navigation. | tools/_/app/nav.php, tools/mvc/get.php |
9
- | [Tools SAML SSO Consumer & Persona-Gated Auth (App_Auth)](features/saml-sso-auth.md) | `App_Auth` is the Tools app's authentication layer: it consumes the SAML gateway `?saml=` handoff (see the 2.0 SAML downstream integration contract), establishe | tools/_/app/auth.php, tools/mvc/sso/initiate/get.php, tools/mvc/sso/get.php, tools/mvc/login/get.php, tools/mvc/login/post.php, tools/mvc/logout/get.php, tools/mvc/get.php, tools/config.production.ini |
9
+ | [Tools SAML SSO Consumer & Persona-Gated Auth (App_Auth)](features/saml-sso-auth.md) | `App_Auth` is the Tools app's authentication layer: it consumes the SAML gateway `?saml=` handoff (see the 2.0 SAML downstream integration contract), establishe | tools/_/app/auth.php, tools/mvc/sso/initiate/get.php, tools/mvc/sso/get.php, tools/mvc/login/get.php, tools/mvc/login/post.php, tools/mvc/logout/get.php, tools/mvc/get.php, tools/config.production.ini, tools/config.local.ini |
10
10
  | [Talos Pricing UI (Onboarding, Dashboard, Benchmarks, Cost Factors + Estimator)](features/talos-pricing-ui.md) | The 1.0 (tools app) face of the **Talos Pricing Platform** — a "Talos Pricing" nav folder with four pages plus a client-side estimate engine. | tools/_/app/nav.php, tools/_/app/talos/estimator.php, tools/mvc/talos/onboarding/get.php, tools/mvc/talos/onboarding/post.php, tools/mvc/talos/pricing/get.php, tools/mvc/talos/benchmarks/get.php, tools/mvc/talos/factors/get.php, tools/mvc/talos/factors/post.php, tools/assets/css/style.css |
11
11
  | [Deploying Tools to Elastic Beanstalk (PHP 8.5 / Amazon Linux 2023)](workflows/deploy-to-elastic-beanstalk-al2023.md) | How the **Tools** 1.0 app boots on Elastic Beanstalk running `PHP 8.5 on 64bit Amazon Linux 2023/4.13.1 (aarch64)`. | tools/.ebextensions/004_http_to_https.config, tools/.ebextensions/006_mount-s3fs.config, tools/.ebextensions/007_setup_export_cache_folders.config, tools/.ebextensions/008_setup_ldap.config, tools/.ebextensions/009_setup_phpini.config, tools/.ebextensions/020_setup_git_libraries.config, tools/.ebextensions/050_register_instance_to_shared_application_load_balancer.config, tools/ebs/git.json |
@@ -6,13 +6,14 @@ project: Tools
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-06-29
9
+ updated: 2026-06-30
10
10
  owners: [jcardinal]
11
11
  files:
12
12
  - tools/_/app/nav.php
13
13
  - tools/mvc/get.php
14
14
  related:
15
15
  - ./persona-gated-navigation.md
16
+ - ./saml-sso-auth.md
16
17
  - ../architecture.md
17
18
  - ../../library/features/mvc-page-pattern-and-app-skeleton.md
18
19
  ---
@@ -69,9 +70,26 @@ $res = App_Database::query($sql, 'db_team');
69
70
  $rows = App_Database::buildArrayOfRows($res, false);
70
71
  ```
71
72
 
73
+ ## Gotcha — a page CANNOT set HTTP status/headers (preloader already flushed)
74
+ `App_FrameworkIndex::render()` turns on the preloader and calls `App_Page::flushCapture()`
75
+ (`ob_end_flush + flush`, `library/app/page.php:213`) **before** `body() → App_MVC::loadFile()` runs
76
+ your mvc page. So by the time any page executes, **headers are already sent**. Calling
77
+ `http_response_code()`, `header()`, or `session_regenerate_id()` from inside a page **fatals**
78
+ ("headers already sent"), and the error handler escalates that to an uncaught `ErrorException` → a
79
+ bare **500**. Always guard:
80
+
81
+ ```php
82
+ if (!headers_sent()) { http_response_code(401); } // matches App_MVC::routeTo's own check
83
+ ```
84
+
85
+ `establishSession()` (auth.php) guards `session_regenerate_id` for the same reason; the SSO failure
86
+ path hit this too — see [saml-sso-auth](./saml-sso-auth.md). The clean long-term fix is to handle
87
+ status-setting routes **before any output** and `exit`.
88
+
72
89
  ## Autoloader
73
90
  `App_Foo_Bar` → `app/foo/bar.php`, **all lowercase** (per the library CLAUDE.md). E.g.
74
91
  `App_Talos_Estimator` → `_/app/talos/estimator.php`.
75
92
 
76
93
  ## Change history
94
+ - 2026-06-30 — Added the preloader-flush gotcha: pages run after `App_Page::flushCapture()` flushes the output buffer, so `http_response_code`/`header`/`session_regenerate_id` from inside a page fatal on headers-already-sent (escalates to a 500) — guard with `!headers_sent()`. Surfaced by the SSO failure-path 500 fix. (jcardinal)
77
95
  - 2026-06-29 — Documented tools MVC routing (GET→get.php / POST→post.php; writes via mvc/<route>/post.php, not the legacy actionHandler path), Origin/Referer CSRF (no token field), `App_Database` query/row/txn API + `sqlEscape()` name, the by-reference row-helper warning gotcha, the persona-narrowing gotcha, and the lowercase autoloader mapping. Discovered building the Talos Pricing UI. (jcardinal)
@@ -6,7 +6,7 @@ project: Tools
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-06-26
9
+ updated: 2026-06-30
10
10
  owners: [jcardinal]
11
11
  files:
12
12
  - tools/_/app/auth.php
@@ -17,6 +17,7 @@ files:
17
17
  - tools/mvc/logout/get.php
18
18
  - tools/mvc/get.php
19
19
  - tools/config.production.ini
20
+ - tools/config.local.ini
20
21
  related:
21
22
  - ../architecture.md
22
23
  - ../../library/features/app-sso-initiation.md
@@ -49,14 +50,30 @@ auto-created.
49
50
  ### Consuming the `?saml=` handoff (`mvc/sso/get.php` → `App_Auth`)
50
51
  1. Length-guard `$_GET['saml']`, then base64-decode → `json_decode` → read `payload.client`
51
52
  and `payload.user`.
52
- 2. Decrypt each with `App_String::decryptWithKey()` using config `[saml] api_secret_access_token`,
53
- with a **dual-key retry** against `api_secret_access_token_previous` (supports gateway key
54
- rotation). This interoperates byte-for-byte with the 2.0 `_String::encryptWithKey()` that
55
- produced the token (see the library `App_String` crypto methods).
53
+ 2. Decrypt each with `App_String::decryptWithKey()` (AES-256-CBC; see crypto format below) via
54
+ `App_Auth::decryptHandoffValue()`, with a **current → previous dual-key fallback** so an
55
+ in-flight gateway key rotation still decodes. **Keys come from `Core.Parameters` at runtime,
56
+ not from config** — see "Handoff key sourcing" below. This interoperates byte-for-byte with
57
+ the 2.0 `_String::encryptWithKey()` that produced the token.
56
58
  3. Validate the decrypted **client uuid == configured `true_client_uuid`** via `hash_equals`,
57
59
  and that both decrypted values match a UUID regex.
58
60
  4. Load the active Client_True user: `WHERE uuid = ? AND isActive = 1`. No match → fail closed.
59
61
 
62
+ ### Handoff key sourcing (`handoffKeys()` → `Core.Parameters`)
63
+ The shared secret is **rotated regularly**, so the static `config.*.ini [saml]` token goes stale —
64
+ that staleness was exactly why `decryptHandoffValue()` returned `false` and production login failed
65
+ (the payload decoded to the correct iv+base64 shape; it was a **key** mismatch, not a format bug).
66
+ - `App_Auth::handoffKeys()` (new private helper) `SELECT \`key\`,\`value\` FROM Parameters WHERE
67
+ \`key\` IN ('API_SECRET_ACCESS_TOKEN','API_SECRET_ACCESS_TOKEN_PREVIOUS')` over the **`db_toga2core`**
68
+ connection (2.0 PROD Core DB; `const CORE_DB = 'db_toga2core'`). Result is **cached per request**.
69
+ `\`key\`` and `\`value\`` are SQL reserved words → **must be backticked**.
70
+ - `decryptHandoffValue()` then tries the current key, falling back to previous (rotation window).
71
+ Its **public signature is unchanged** — only `mvc/sso/get.php` calls it.
72
+ - The old `[saml] api_secret_access_token` / `_previous` config values are now **dead** (removable).
73
+ `[saml] true_client_uuid` is **still used** (`sso/get.php:80-84`) and stays.
74
+ - See [Core.Parameters key/value store](#coreparameters-key-value-store) for the table shape and
75
+ which DB cluster it lives on.
76
+
60
77
  ### Session establishment (`establishSession()`)
61
78
  - Calls `session_regenerate_id(true)` **guarded by `if (!headers_sent())`** (see gotcha), then
62
79
  caches the user and **persona names** in `$_SESSION`.
@@ -88,22 +105,51 @@ removed.
88
105
  **inside** the page render (`frameworkindex → body → loadFile`) after `common/header.php` has
89
106
  emitted output, so `session_regenerate_id(true)` fatals ("cannot be regenerated after headers
90
107
  already sent"). Fixed by guarding with `if (!headers_sent())`. **Known architectural limitation:**
91
- the failure path (`tools_ssoFail → http_response_code` at `mvc/sso/get.php:23`) *also* fatals on
92
- headers-sent; the proper long-term fix is to route `/sso` **before any output** and `exit`. Any
93
- 1.0 app adopting this consumer pattern inherits this — see
94
- [App_Sso](../../library/features/app-sso-initiation.md).
108
+ the proper long-term fix is to route `/sso` **before any output** and `exit`. Any 1.0 app adopting
109
+ this consumer pattern inherits this — see [App_Sso](../../library/features/app-sso-initiation.md)
110
+ and the [preloader-flush gotcha](./mvc-data-access-patterns.md) for the general rule.
111
+ - **Failure path also fataled → fixed (2026-06-30).** `tools_ssoFail()` called
112
+ `http_response_code(401)` to render the "Authentication failed" page, which itself fataled with
113
+ "headers already sent" and the error handler escalated it to an uncaught `ErrorException` → a bare
114
+ **500** instead of the intended 401 page. Root cause: `App_FrameworkIndex::render()` turns on the
115
+ preloader and runs `App_Page::flushCapture()` (`ob_end_flush + flush`, `library/app/page.php:213`)
116
+ **before** `body()/App_MVC::loadFile()` runs the mvc page. Fixed with the same
117
+ `if (!headers_sent()) { http_response_code(401); }` guard (matches `App_MVC::routeTo`'s own check).
95
118
  - **No app-side replay defense.** The handoff token carries no nonce/timestamp the app verifies.
96
119
  Recommend the gateway embed `iat` + `jti`.
97
120
 
121
+ ## Crypto format (`App_String::encryptWithKey` / `decryptWithKey`)
122
+ `library/app/string.php:1075-1094`, AES-256-CBC: `encryptWithKey` returns
123
+ `base64( iv . openssl_encrypt(..., flag 0) )` — i.e. a base64 envelope whose body is the raw IV
124
+ followed by openssl's own base64 ciphertext; `decryptWithKey` reverses it. **Interoperable
125
+ byte-for-byte with the 2.0 `_String::decryptWithKey()`** that the SAML gateway uses to produce the
126
+ handoff token. The 2026-06-30 login failure decoded to the exact expected iv+base64 shape, proving
127
+ the issue was a stale **key**, not a cipher/format mismatch.
128
+
129
+ ## Core.Parameters key/value store
130
+ The platform's rotating key/value parameter store, on the **2.0 PROD Core DB** (`Core` schema).
131
+ Columns: `id`, `uuid`, `\`key\`` (UNIQUE), `\`value\`` — `key`/`value` are reserved words and **must
132
+ be backticked**. Holds the SSO/API shared secrets `API_SECRET_ACCESS_TOKEN` and
133
+ `API_SECRET_ACCESS_TOKEN_PREVIOUS`. The tools app reaches it via the **`db_toga2core`** connection
134
+ (`[database_toga2core]`, dbname `Core`, prod core cluster in `us-west-2`) — a cluster **distinct**
135
+ from the 1.0 prod-cluster and from the 2.0 client cluster that backs `db_true`/`Client_True`.
136
+
98
137
  ## Config keys
99
138
 
100
- `[database_true]` (read-only Client_True); `[saml]` `api_secret_access_token` /
101
- `api_secret_access_token_previous`, `client_authentication_uuid`, `domain_uuid`,
102
- `true_client_uuid` (plus the IdP/ACS urls used by initiation); `[internal]` `dev_mode`. **Secret
103
- location:** `config.production.ini` holds the plaintext production secrets — including the shared
104
- Core `API_SECRET_ACCESS_TOKEN` — in its `[saml]` section (committed; developer explicitly accepted
105
- this). Document **where** they live, never the values.
139
+ `[database_true]` (read-only Client_True); **`[database_toga2core]`** — new requirement: alias
140
+ `db_toga2core`, dbname `Core`, the prod core cluster host (`production-core-cluster…us-west-2…`),
141
+ needed in **each** `config.*.ini` so the handoff keys can be read at runtime (developer is adding
142
+ creds). `[saml]`: `true_client_uuid` (still used), `client_authentication_uuid`, `domain_uuid`,
143
+ plus the IdP/ACS urls used by initiation; `[internal]` `dev_mode`. The old `[saml]
144
+ api_secret_access_token` / `_previous` are now **dead** (superseded by `Core.Parameters`).
145
+
146
+ **Secret location.** Rotating secrets are now sourced from `Core.Parameters` **at runtime** rather
147
+ than static config — an improvement over the old committed `config.production.ini [saml]` tokens.
148
+ A **pre-existing reused plaintext DB password** across the `config.*.ini [database_*]` sections
149
+ remains (already noted in tools knowledge) — **flag for rotation.** Document **where** secrets live,
150
+ never the values.
106
151
 
107
152
  ## Change history
153
+ - 2026-06-30 — Fixed prod SSO login failure: handoff decrypt keys now sourced from `Core.Parameters` (rotated `API_SECRET_ACCESS_TOKEN`/`_PREVIOUS`) via new `App_Auth::handoffKeys()` over `db_toga2core` (per-request cached, current→previous fallback), instead of the stale committed `config [saml]` tokens — the stale key was the root cause. Added `[database_toga2core]` connection requirement to every config and documented the AES-256-CBC crypto format. Fixed `tools_ssoFail()` 500→intended 401: `http_response_code(401)` fataled on headers-already-sent (preloader flushes before mvc page runs); guarded with `!headers_sent()`. (jcardinal)
108
154
  - 2026-06-26 — Wired up real SSO initiation via the new 1.0 `App_Sso` library class (`/sso/initiate`), replacing the `initiation_url` placeholder; registered the fixed-uuid Core.Domains return row (dbchanges2 `2026-06-26a`); fixed the `session_regenerate_id` headers-already-sent fatal (guarded with `!headers_sent()`) and documented the mid-render failure-path limitation; collapsed home → `/login` to one-step sign-in; noted prod secrets live in `config.production.ini [saml]` (jcardinal)
109
155
  - 2026-06-25 — Built App_Auth: SAML `?saml=` handoff consumer with dual-key decrypt, hash_equals client-uuid check, fail-closed 401, persona-cached session (HttpOnly+SameSite=Lax), and a double-gated dev bypass. Initiation + replay defense left as open items (jcardinal)
@@ -99,6 +99,19 @@ Before creating a new class in a 1.0 repo:
99
99
  3. Follow the naming convention exactly, including the type segment.
100
100
  4. If you are adding a class to the `library` core repo, be aware it affects all 1.0 apps — review `knowledge/1.0/apps/library/architecture.md` first.
101
101
 
102
+ ## Schema migrations — always in `dbchanges`
103
+
104
+ **Application repos (`worker`, `togadesk`, `togaview`, `tools`, and any other 1.0 app)
105
+ contain no standalone `.sql` migration or schema files.** All schema changes for any
106
+ 1.0 database — table creation, `ALTER` statements, index changes, seed data — go in the
107
+ **`dbchanges`** repo.
108
+
109
+ This rule applies regardless of which application repo the feature lives in. Even if you
110
+ are adding a column that only `worker` reads, the `.sql` file goes in `dbchanges`.
111
+
112
+ Never create a `.sql` file inside a 1.0 application repo. The SQL that belongs in those
113
+ repos is only query strings embedded in PHP code — not standalone migration files.
114
+
102
115
  ## Error handling in controllers
103
116
 
104
117
  Controllers must not let exceptions bubble up to the framework unhandled. Catch specific exceptions, log them, and return an appropriate response.
@@ -39,6 +39,13 @@ processes background jobs. It's a `_underscore` 2.0 app (`index.php` is just
39
39
  | S3 `agilant-worker-fallback` | Failed webhook payloads (`webhook-fallback/{uuid}.json`, 7-day lifecycle). |
40
40
  | API Gateway `WorkerWebhookIngestion` | `webhook.togahub.com` → Lambda. Lambda Function URLs are blocked by an AWS Org SCP, so API Gateway is the only public entry point. |
41
41
 
42
+ ## Critical repo rule — no SQL migration files
43
+
44
+ **This repo contains PHP and Python only. Never create `.sql` schema-migration files here.**
45
+ All table creation, `ALTER` statements, index changes, and seed data — including for
46
+ `Core.WorkerJobs` and `Core.CronJobs` — go in the `dbchanges2` repo using its
47
+ `YYYY-MM-DD<letter> - <Description>.sql` naming convention.
48
+
42
49
  ## MySQL tables
43
50
 
44
51
  **`Core.WorkerJobs`** — unified execution record. Key columns: `uuid`,
@@ -22,6 +22,14 @@ related:
22
22
 
23
23
  ## Database/SQL
24
24
 
25
+ ### Migration files belong in `dbchanges2`
26
+
27
+ SQL query strings embedded in PHP code (SELECTs, INSERTs, UPDATEs) live in this repo as
28
+ part of normal PHP files. **Standalone `.sql` migration files do not.** Any schema change
29
+ — CREATE TABLE, ALTER TABLE, new index, seed rows — must be placed in the `dbchanges2`
30
+ repo, not here. See `2.0/standards/framework-rules.md` § Schema migrations for naming
31
+ conventions.
32
+
25
33
  ### Table Design
26
34
 
27
35
  #### **Order of Fields**
@@ -86,6 +86,22 @@ return [
86
86
  ];
87
87
  ```
88
88
 
89
+ ## Schema migrations — always in `dbchanges2`
90
+
91
+ **Application repos (`worker2`, `api2`, `_underscore`) contain no standalone `.sql`
92
+ migration or schema files.** All schema changes for any 2.0 database — table creation,
93
+ `ALTER` statements, index changes, seed data — go in the **`dbchanges2`** repo.
94
+
95
+ File naming in `dbchanges2`: `YYYY-MM-DD<letter> - <Description>.sql`
96
+ (e.g. `2026-06-30a - Add WorkerJobs failureCode column.sql`).
97
+
98
+ This rule applies regardless of which application repo the feature lives in. Even if you
99
+ are adding a column that only `worker2` reads, the `.sql` file goes in `dbchanges2`.
100
+
101
+ Never create a `.sql` file inside `worker2`, `api2`, `_underscore`, or any other 2.0
102
+ application repo. The SQL that belongs in those repos is only query strings embedded in
103
+ PHP code — not standalone migration files.
104
+
89
105
  ## Checking dependencies before touching shared code
90
106
 
91
107
  `dependsOn` in `knowledge/registry.json` means a repo extends or depends on another repo's classes. Before modifying a class in a dependency repo (e.g. `_underscore` core):
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.235",
3
+ "version": "1.0.237",
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",