toga-ai 1.0.586 → 1.0.588

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.
@@ -2,7 +2,7 @@
2
2
 
3
3
  | Doc | Summary | Files |
4
4
  |-----|---------|-------|
5
- | [Tools (1.0 Internal-Tools App) Architecture](architecture.md) | **Tools** is a standalone 1.0 (`App_`) application that houses many small internal tools behind simple interfaces, gated by Client_True staff persona. | tools/index.php, tools/_/app/framework.php, tools/_/app/frameworkindex.php, tools/assets/img/favicon/favicon.ico, tools/assets/img/favicon/favicon-32x32.png, tools/assets/img/favicon/favicon-16x16.png, tools/assets/img/favicon/apple-touch-icon.png, tools/_/app/auth.php, tools/_/app/nav.php, tools/common/header.php, tools/common/footer.php, tools/mvc/get.php, tools/mvc/_TEMPLATE/get.php, tools/docs/ADDING_A_TOOL.md, tools/assets/css/theme.css, tools/assets/js/theme.js |
5
+ | [Tools (1.0 Internal-Tools App) Architecture](architecture.md) | **Tools** is a standalone 1.0 (`App_`) application that houses many small internal tools behind simple interfaces, gated by Client_True staff persona. | tools/index.php, tools/.ebextensions/020_setup_git_libraries.config, tools/_/app/framework.php, tools/_/app/frameworkindex.php, tools/assets/img/favicon/favicon.ico, tools/assets/img/favicon/favicon-32x32.png, tools/assets/img/favicon/favicon-16x16.png, tools/assets/img/favicon/apple-touch-icon.png, tools/_/app/auth.php, tools/_/app/nav.php, tools/common/header.php, tools/common/footer.php, tools/mvc/get.php, tools/mvc/_TEMPLATE/get.php, tools/docs/ADDING_A_TOOL.md, tools/assets/css/theme.css, tools/assets/js/theme.js |
6
6
  | [ClickUp Sprint Dashboard (tools /clickup/react)](features/clickup-sprint-dashboard.md) | A native **ClickUp sprint dashboard** in the 1.0 `tools` app at route `/clickup/react`, built for a wall-mounted **TV display** ("TOGa IQ" internal sprint metri | tools/_/app/clickup/sprint.php, tools/v2/sprints/tile/index.php, tools/v2/sprints/current/index.php, tools/v2/sprints/status-breakdown/index.php, tools/v2/sprints/worktype-breakdown/index.php, tools/v2/sprints/points-by-dev/index.php, tools/v2/sprints/burndown/index.php, tools/assets/clickup/sprint-dashboard.html, tools/mvc/clickup/react/get.php, tools/mvc/login/get.php, tools/_/app/nav.php |
7
7
  | [CloudFront Client Setup](features/cloudfront-client-setup.md) | An SSO-gated admin tool at **`/devops/cloudfront-clients`** in the Tools 1.0 app that onboards a client onto **CloudFront + Route 53 across multiple AWS account | tools/_/app/devops/cloudfront.php, tools/mvc/devops/cloudfront-clients/get.php, tools/mvc/devops/cloudfront-clients/post.php, tools/assets/js/cloudfront-clients.js, tools/assets/css/cloudfront-clients.css, tools/_/app/nav.php, tools/_/app/frameworkindex.php, tools/config.production.ini |
8
8
  | [Design Demo Admin](features/design-demo-admin.md) | A self-serve admin UI at **`/design`** in the SSO-protected **Tools** app that lets the design team publish self-contained "Claude Design" HTML exports as **ver | tools/_/app/design/github.php, tools/mvc/design/get.php, tools/mvc/design/post.php, tools/assets/css/design.css, tools/assets/js/design.js, tools/_/app/frameworkindex.php, tools/_/app/nav.php, tools/composer.json |
@@ -16,4 +16,4 @@
16
16
  | [Talos Knowledge Base Admin UI (KB Documents + Vocabulary)](features/talos-kb-documents-admin.md) | > **PER-AI-MODEL, DATA-DRIVEN SCOPING (2026-07-29).** The KB-documents and Vocabulary admin > UIs were refactored from a single hard-coded **"development-team"* | tools/mvc/talos/kb-documents/get.php, tools/mvc/talos/kb-documents/post.php, tools/mvc/talos/knowledge-bases/get.php, tools/mvc/talos/knowledge-bases/post.php, tools/mvc/talos/vocabulary/get.php, tools/mvc/talos/vocabulary/post.php, tools/_/app/talos/s3.php, tools/_/app/talos/bedrock.php, tools/_/app/pg.php, tools/_/app/model/true/aimodels.php, tools/_/app/model/true/vectorindexes.php, tools/_/app/model/true/aimodels_vectorindexes.php, tools/_/app/worker.php, tools/_/app/nav.php, tools/config.production.ini, tools/config.alpha.ini, tools/.platform/httpd/conf.d/timeouts.conf, tools/.platform/hooks/prebuild/01-install-php-pgsql.sh, tools/.platform/hooks/postdeploy/01-restart-php.sh |
17
17
  | [Talos Pricing UI (Contracts, Pricing Dashboard, Usage, Settings + 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 the estimate engine. | tools/_/app/nav.php, tools/_/app/talos/estimator.php, tools/_/app/talos/usage.php, tools/mvc/talos/contracts/get.php, tools/mvc/talos/contracts/post.php, tools/mvc/talos/pricing/get.php, tools/mvc/talos/usage/get.php, tools/mvc/talos/settings/get.php, tools/mvc/talos/settings/post.php, tools/assets/css/style.css |
18
18
  | [App-Wide Colour Theme (light / dark / auto)](features/theme-light-dark.md) | A **light / dark / auto** colour theme for the *entire* Tools app, built as a single semantic-token layer (`assets/css/theme.css`) rather than per-page edits. | tools/assets/css/theme.css, tools/assets/js/theme.js, tools/_/app/frameworkindex.php, tools/_/app/nav.php, tools/common/header.php, tools/mvc/login/get.php, tools/assets/css/style.css, tools/assets/css/design.css, tools/assets/css/login.css, tools/assets/css/cloudfront-clients.css, tools/assets/css/gchart.css, tools/mvc/talos/usage/get.php, tools/mvc/talos/pricing/get.php, tools/mvc/talos/contracts/get.php, tools/mvc/clickup/dashboard/get.php, tools/mvc/email-migration/notify/post.php |
19
- | [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, tools/ebs/setup_export_cache_folders.php, tools/_/app/frameworkindex.php |
19
+ | [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/003_pgsql_hook_optional.config, tools/.ebextensions/020_setup_git_libraries.config, tools/.ebextensions/050_register_instance_to_shared_application_load_balancer.config, tools/.platform/hooks/prebuild/01-install-php-pgsql.sh, tools/.platform/hooks/prebuild/02-install-php-redis.sh, tools/ebs/git.json, tools/ebs/setup_export_cache_folders.php, tools/_/app/framework.php, tools/_/app/frameworkindex.php |
@@ -10,6 +10,7 @@ updated: 2026-08-14
10
10
  owners: [jcardinal, kyalamarthi]
11
11
  files:
12
12
  - tools/index.php
13
+ - tools/.ebextensions/020_setup_git_libraries.config
13
14
  - tools/_/app/framework.php
14
15
  - tools/_/app/frameworkindex.php
15
16
  - tools/assets/img/favicon/favicon.ico
@@ -66,8 +67,24 @@ App_Framework_Tools::renderIndex();`. App-specific framework subclasses
66
67
  `App_Framework_Tools` / `App_FrameworkIndex_Tools` live in `_/app/`. The 1.0 folder-based MVC
67
68
  page pattern is unchanged from the library skeleton (see the library feature doc).
68
69
 
69
- `App_Framework_Tools::initialize()` sets the session cookie HttpOnly + SameSite=Lax before
70
- calling the parent initialize.
70
+ `App_Framework_Tools::initialize()` sets the session cookie HttpOnly + SameSite=Lax, then
71
+ `App_Framework_Tools::startSession()` starts the session on the **shared ElastiCache Redis
72
+ cluster** (phpredis `rediscluster` handler, cluster-mode config endpoint
73
+ `asi-session-cache…clustercfg.usw2.cache.amazonaws.com:6379`, shared with the `vision` app),
74
+ configured at runtime via `ini_set()` (`session.save_handler=rediscluster`,
75
+ `session.save_path=seed[]=<endpoint>:6379`, `session.gc_maxlifetime=345600`), guarded by
76
+ `extension_loaded('redis')`. It calls `session_start()` itself and sets
77
+ `App_Framework::$useSessions=false` so the parent's `initialize()` does not double-start the
78
+ session (verified that flag's only consumer is the parent's `session_start` guard).
79
+
80
+ **Ordering is load-bearing:** `startSession()` runs **before** `parent::initialize()` installs
81
+ App_Error's fatal handler, so a Redis failure degrades to file sessions via a graceful
82
+ `@session_start()` fallback instead of a site-wide 500. **Why this exists:** sessions used to
83
+ run on the default **file** handler on **per-instance local disk**; because prod is
84
+ multi-instance behind an ALB, every deploy / instance replacement wiped the local session
85
+ files and logged all users out. Prod-verified 2026-08-14: a redeploy no longer logs users out.
86
+ How the handler is wired at the platform level (INI-file parser drops `seed[]`, fpm overrides
87
+ `/etc/php.d`, phpredis install) is in `workflows/deploy-to-elastic-beanstalk-al2023.md`.
71
88
 
72
89
  ## Layout (single-style app)
73
90
 
@@ -133,6 +150,12 @@ Create `mvc/<folder>/<tool>/get.php` (copy `mvc/_TEMPLATE/get.php`), add one ent
133
150
  `config.*`) — see the [1.0 back-end security standard](../../standards/backend-php.md).
134
151
  **Action still owed:** rotate every secret in `config.production.ini` — the file was
135
152
  reachable, so treat all of it as compromised. (Location only — no values recorded here.)
153
+ - **Hardcoded GitHub Personal Access Token committed in plaintext (rotation owed).**
154
+ `.ebextensions/020_setup_git_libraries.config` contains a GitHub PAT in plaintext, committed
155
+ to the repo and therefore exposed in git history. **Action owed:** rotate the token and move
156
+ it to an EB environment property (read via `getenv()`/`$_SERVER`/`$_ENV` per the deploy
157
+ workflow's clear_env gotcha); a rotation does not un-expose git history. (Location only — no
158
+ value recorded here.)
136
159
  - **SSO initiation + replay defense are open items** — see `features/saml-sso-auth.md`.
137
160
  - **Unproxied third-party polling from the browser (`/clickup/react`).** The monitor column pulls
138
161
  ~250 KB from OneUptime every 5s — ~3 MB/min **per open tab** — to render about six numbers, and
@@ -147,6 +170,12 @@ Create `mvc/<folder>/<tool>/get.php` (copy `mvc/_TEMPLATE/get.php`), add one ent
147
170
  composer package must `require_once` the autoloader itself.
148
171
 
149
172
  ## Change history
173
+ - 2026-08-14 — Corrected the session model: sessions were on the file handler on per-instance
174
+ local disk (not Redis as previously assumed), so every deploy logged everyone out; moved to the
175
+ shared ElastiCache Redis `rediscluster` handler via `App_Framework_Tools::startSession()`
176
+ (runtime `ini_set()`, `$useSessions=false`, started before App_Error installs so a Redis failure
177
+ degrades gracefully). Flagged the plaintext GitHub PAT in `020_setup_git_libraries.config` for
178
+ rotation + move to an EB env property. (jcardinal)
150
179
  - 2026-08-14 — Recorded that `/clickup/react` is now an unattended wall display holding a
151
180
  full-screen colour state indefinitely, carrying the app's only cross-origin browser dependency
152
181
  (OneUptime public status-page API) plus per-iframe permission policy
@@ -123,7 +123,8 @@ poll's:
123
123
  - The **first successful poll is the baseline**, so opening the page mid-outage is **silent**
124
124
  (the standing tint, being level-triggered, still shows immediately).
125
125
  - A state that simply persists never re-alarms.
126
- - A full recovery to operational is **silent** (the monitor drops out of the row set entirely).
126
+ - A full recovery to operational is **visually silent** (no popup, no tint) but now plays a
127
+ distinct **recovery chime** — see the recovery event below.
127
128
  - It fires on **any change of non-operational state** — `r.role !== previous.get(r.key)`, where
128
129
  the row set only ever holds non-operational monitors, so good→degraded, good→offline,
129
130
  degraded→offline **and offline→degraded** all alert. An earlier version compared severity
@@ -134,11 +135,30 @@ poll's:
134
135
  The event presentation, held for `ALERT_HOLD_MS` (5s):
135
136
 
136
137
  - **Sound** — Web Audio beeps, synthesised at runtime, so there is **no audio asset to ship**.
137
- **1 beep @ 720 Hz** degraded, **2 beeps @ 990 Hz** offline. The beep count describes the state
138
- the monitor is in **now**, so an offline → degraded change beeps **once**. Pitch differs too,
139
- so severity is recognisable across a room without counting.
140
- - **Popup** — a centre overlay naming each affected monitor.
138
+ Each role's sound is defined in a single `ALERT_SOUND` table keyed by role, read by a
139
+ generalised `playBeeps(role)` (signature unchanged: role → bool). Each entry carries a beep
140
+ **count**, base **freq** (Hz), a per-beep **step** (Hz added to each successive beep — `0` =
141
+ flat, positive = a rising chime), duration/gap/volume, and a **wave** shape. Current tuned
142
+ values: **warning (degraded) = 3× square @ 720 Hz** (flat), **critical (offline) = 4× square
143
+ @ 410 Hz** (flat), **recovered = 3× square rising chime 620 → 800 → 980 Hz** (freq 620,
144
+ step 180). Distinct count, pitch and the rising vs. flat contour make each event
145
+ recognisable across a room without looking. The three sounds were tuned by ear in a
146
+ throwaway standalone "Monitor Sound Lab" browser page and the final numbers ported in — the
147
+ lab is not part of the repo.
148
+ - **Popup** — a centre overlay naming each affected monitor. **Bad-state events only** —
149
+ recovery fires no popup.
141
150
  - **Blink** — the tint element gains `.is-blinking` for the 5s pulse, then the colour **holds**.
151
+ **Bad-state events only** — recovery fires no blink and no tint.
152
+
153
+ **Recovery event — audio-only, by design.** `useMonitorAlerts` also computes `recovered` = keys
154
+ that were non-operational in the previous poll and have **dropped out of `model.rows`** this poll,
155
+ and calls `playBeeps('recovered')` (the rising chime) for them. It deliberately fires **no popup
156
+ and no screen tint**: the level-triggered standing tint already clears itself when a row leaves
157
+ the set, and a green "all clear" popup/flash would fight the "the board only shouts when something
158
+ is wrong" design. So recovery is silent **visually** but audible. Two edge rules: the **first
159
+ poll is still the baseline** (opening the page mid-outage does not chime), and if a bad-state
160
+ change and a recovery land in the **same 5s poll the bad-state alert wins** that tick, avoiding
161
+ overlapping sounds (simultaneous transitions are rare).
142
162
 
143
163
  **State — `standingRole(model)`** returns `critical` if any row is offline, else `warning` if any
144
164
  is degraded, else `null` (no tint). The tint element only renders while `standingRole` is
@@ -148,8 +168,10 @@ Note the two can legitimately show different colours at once: the tint always re
148
168
  **worst-current** state while the popup reflects **the event**, so a degraded-event popup can sit
149
169
  on a red screen because something else is still offline. That is correct, not a bug.
150
170
 
151
- Verified across full cycles: beep sequence 720 / 990 990 / 720, silent on unchanged and on
152
- recovery, popup held 4982 ms and 4955 ms against the 5s spec; amber standing → red when offline
171
+ Verified across full cycles (against the original 1-beep-720 / 2-beep-990 tuning; the current
172
+ values are 3×720 / 4×410 / recovered 620→800→980): distinct sounds per event, silent on
173
+ unchanged, recovery now chimes but stays visually silent, popup held 4982 ms and 4955 ms against
174
+ the 5s spec; amber standing → red when offline
153
175
  joined (priority) → amber when offline cleared → no tint on full recovery, with the blink
154
176
  stopping after 5s and the colour held. Circle verified against the DOM: `#0ca30c` all-clear,
155
177
  `#fab219` degraded only, `#d03b3b` with 1 degraded + 1 offline, and `textNodesInSvg = 0`.
@@ -209,8 +231,9 @@ reduced-motion viewer never lands on a fully-invisible (opacity 0) glow.
209
231
  OneUptime call returns HTTP 200 with a malformed or empty body, `buildMonitorModel` yields
210
232
  total 0, empty buckets and empty rows. The status pill correctly degrades to "Unknown", but
211
233
  the affected-list card still reads **"All 0 monitors are operational"**, and any
212
- previously-degraded monitor silently disappears **with no alert** (recovery is silent by
213
- design, and an empty row set is indistinguishable from a full recovery). Since the standing
234
+ previously-degraded monitor silently disappears — and, worse now that recovery is audible,
235
+ the mass drop-out is **indistinguishable from a full recovery**, so the panel plays the happy
236
+ **recovery chime** for every vanished monitor on a parse failure. Since the standing
214
237
  tint and the status circle are both driven by that same empty row set, a parse failure also
215
238
  **clears a standing red screen back to green** — the most reassuring possible display of a
216
239
  total loss of visibility. Recommended fix: treat "zero resources parsed" as an **error
@@ -231,6 +254,18 @@ summary, and caches ~5s. Rationale:
231
254
  alerting really wants (today the baseline resets every time a tab reloads).
232
255
 
233
256
  ## Change history
257
+ - 2026-08-14 — Generalised alert-sound synthesis and added a **recovery chime**. The old
258
+ `BEEP_COUNT`/`BEEP_HZ`/`BEEP_MS`/`BEEP_GAP_MS`/`BEEP_VOLUME` constants and the square-wave-only
259
+ `playBeeps` were replaced by a single per-role `ALERT_SOUND` table (count / freq / per-beep
260
+ `step` / duration / gap / volume / `wave`) read by a generalised `playBeeps(role)` (signature
261
+ unchanged). Retuned to degraded 3×square@720, offline 4×square@410 (was degraded 1×@720,
262
+ offline 2×@990). Recovery — a monitor dropping out of the non-operational row set — now plays
263
+ a distinct rising chime (620→800→980, freq 620/step 180) and is **audio-only by design**: no
264
+ popup, no tint (the standing tint already self-clears; a green "all clear" flash would fight
265
+ the "only shouts when wrong" design). First poll stays the silent baseline; a bad-state change
266
+ and a recovery in the same tick let the bad-state alert win. Sounds were tuned by ear in a
267
+ throwaway standalone browser "Sound Lab" (not a repo file) and ported in. Corrects the prior
268
+ "recovery is silent" and old beep-count/pitch documentation. (jcardinal)
234
269
  - 2026-08-14 — The standing (level-triggered) edge tint now **slowly breathes** its opacity
235
270
  0→1→0 on a 7s ease-in-out loop (`@keyframes alert-breathe`) instead of holding a static tint,
236
271
  so a still-broken screen draws a distant eye. The fast 0.7s `.is-blinking` event pulse still
@@ -6,7 +6,7 @@ project: Tools
6
6
  client: shared
7
7
  type: workflow
8
8
  status: active
9
- updated: 2026-08-03
9
+ updated: 2026-08-14
10
10
  owners: [jcardinal, bala]
11
11
  files:
12
12
  - tools/.ebextensions/004_http_to_https.config
@@ -14,10 +14,14 @@ files:
14
14
  - tools/.ebextensions/007_setup_export_cache_folders.config
15
15
  - tools/.ebextensions/008_setup_ldap.config
16
16
  - tools/.ebextensions/009_setup_phpini.config
17
+ - tools/.ebextensions/003_pgsql_hook_optional.config
17
18
  - tools/.ebextensions/020_setup_git_libraries.config
18
19
  - tools/.ebextensions/050_register_instance_to_shared_application_load_balancer.config
20
+ - tools/.platform/hooks/prebuild/01-install-php-pgsql.sh
21
+ - tools/.platform/hooks/prebuild/02-install-php-redis.sh
19
22
  - tools/ebs/git.json
20
23
  - tools/ebs/setup_export_cache_folders.php
24
+ - tools/_/app/framework.php
21
25
  - tools/_/app/frameworkindex.php
22
26
  related:
23
27
  - ../architecture.md
@@ -60,9 +64,23 @@ failed silently (it was wrapped in `ignoreErrors: true`), so `include_path` neve
60
64
  The `application.ini` sets `include_path = ".:/var/www/library"`, upload/post limits
61
65
  (`upload_max_filesize=25M` / `post_max_size=30M`, raised from 16M so multi-MB design-demo HTML
62
66
  exports up to ~25MB post through — see `features/design-demo-admin.md`),
63
- `memory_limit = 2G`, `display_errors`, the `redis.so` extension, and the rediscluster session
64
- handler (`session.save_path` points at the ElastiCache cluster cfg endpoint). Session GC
65
- maxlifetime is 4 days.
67
+ `memory_limit = 2G`, `display_errors`, and loads the `redis.so` extension.
68
+
69
+ > **Do NOT configure the rediscluster session handler in `application.ini` (or any INI file) on
70
+ > this platform — it silently fails.** See the "Redis sessions" section below. Session
71
+ > `save_handler`/`save_path` are now set at **runtime via `ini_set()` in
72
+ > `tools/_/app/framework.php`**, not in `009_setup_phpini.config`. Two INI-file facts drove this
73
+ > (both verified 2026-08-14 on EB PHP 8.5 / AL2023):
74
+ > - **The INI-file parser silently drops a `session.save_path` value of the form
75
+ > `"seed[]=host:port"`** — in *both* `/etc/php.d/*.ini` and the php-fpm pool
76
+ > (`www.conf` `php_value[...]`). `session.save_handler=rediscluster` applies but `save_path`
77
+ > stays `/tmp`, so `session_start()` fatals ("Failed to initialize storage module:
78
+ > rediscluster (path: /tmp)") on **every** request → full outage. CLI `php -d` with the same
79
+ > value works, confirming it is the INI-file parser, not the value itself.
80
+ > - **The php-fpm pool overrides `/etc/php.d/*.ini` at runtime.** `memory_limit=2G` /
81
+ > `display_errors=On` set in `application.ini` were observed running as 256M/Off because the
82
+ > fpm pool `www.conf` wins. Values that must hold have to be set where fpm cannot override
83
+ > them — for session handling, that means `ini_set()` in code.
66
84
 
67
85
  ### Cloning the libraries — bash, not the CodePipeline PHP script
68
86
 
@@ -82,6 +100,52 @@ The replacement is pure bash via a cfn-init `files:` block that writes
82
100
  `ebs/git.json` was corrected to match: branch `_production` for both repos and the resources
83
101
  path set to `/var/app/ondeck/resources`.
84
102
 
103
+ ### Native PHP extensions — prebuild hooks (phpredis, pgsql)
104
+
105
+ Native PECL extensions are installed by `.platform/hooks/prebuild/*.sh` scripts, not
106
+ `.ebextensions`. `02-install-php-redis.sh` installs phpredis (needed for the Redis sessions
107
+ below); `01-install-php-pgsql.sh` installs pgsql (needed only by the Talos "Knowledge Bases"
108
+ create flow). Both target the platform php `php-common-8.5.6`. The durable AL2023 gotchas:
109
+
110
+ - **The package is `php8.5-pecl-redis6`** (phpredis major-version suffix `6`), **not**
111
+ `php8.5-redis`.
112
+ - **A plain `dnf install php8.5-pecl-redis6` drags in the php8.1 serializers.** It pulls the
113
+ igbinary/msgpack deps built for **php8.1** → `php8.1-common`, which **conflicts with the
114
+ platform's `php-common-8.5.6`** over `/etc/php.ini` and `/usr/lib64/php/modules/*.so` (the dnf
115
+ transaction test aborts, failing the deploy). Install with
116
+ `--setopt=install_weak_deps=False --exclude=php8.1-*`.
117
+ - **The versioned package installs the `.so` into a non-platform module dir.** Even once
118
+ installed, `redis.so` (and `pgsql.so`) lands in a dir the platform php does **not** scan. The
119
+ platform php loads from its own `extension_dir` — discover it with
120
+ `php -r 'echo ini_get("extension_dir");'`. The hook copies the `.so` there, writes a scan-dir
121
+ ini enabling it, then **verifies with `php -m`** and fails the deploy on error.
122
+ - **Escape hatch env property.** `02-install-php-redis.sh` honours `REDIS_HOOK_OPTIONAL=1` (EB
123
+ env property) to skip on failure rather than block the whole deploy.
124
+
125
+ **Known-broken sibling — `01-install-php-pgsql.sh` (pgsql).** It has the *same* versioned-package
126
+ / wrong-module-dir bug and currently **fails on fresh AL2023 instances**. It is presently
127
+ **bypassed** via the EB env property `PGSQL_HOOK_OPTIONAL=1`
128
+ (`.ebextensions/003_pgsql_hook_optional.config`). This is a **temporary** measure: it leaves the
129
+ Talos "Knowledge Bases" create flow (the only pgsql consumer) broken. The fix is the same
130
+ install-and-copy-into-`extension_dir` treatment `02-install-php-redis.sh` already received; once
131
+ applied, **delete the `PGSQL_HOOK_OPTIONAL` bypass config.**
132
+
133
+ ### Redis sessions (survive deploys & load balancing)
134
+
135
+ Prod Tools runs multiple EB instances behind an ALB, so PHP sessions must live in a shared store,
136
+ not per-instance local disk. Sessions use the **shared ElastiCache Redis cluster**
137
+ (`asi-session-cache…clustercfg.usw2.cache.amazonaws.com:6379`, cluster-mode config endpoint —
138
+ also used by the `vision` app) via the phpredis `rediscluster` handler.
139
+
140
+ Because the INI-file parser silently drops the `seed[]=host:port` save_path (see the PHP ini
141
+ section above), the handler is configured at **runtime in `tools/_/app/framework.php`**
142
+ (`App_Framework_Tools::startSession()`): `ini_set()` sets
143
+ `session.save_handler=rediscluster`, `session.save_path=seed[]=<cluster cfg endpoint>:6379`, and
144
+ `session.gc_maxlifetime=345600` (4 days), guarded by `extension_loaded('redis')`, then calls
145
+ `session_start()` itself. The full outage story (sessions were previously on the file handler on
146
+ local disk, so every deploy logged everyone out) and the graceful-degrade ordering vs. App_Error
147
+ are in the [architecture doc](../architecture.md).
148
+
85
149
  ### Duplicate config neutralization
86
150
 
87
151
  EB merges every `.config` alphabetically; two configs with **identical** `container_commands`
@@ -185,6 +249,19 @@ cross-instance storage has a local-disk fallback path for dev, where the AWS SDK
185
249
  e.g. `GIT=$(which git || echo /usr/bin/git)`.
186
250
  - **cfn-init `files:` runs before `container_commands`** — the correct pattern for writing a
187
251
  script and then executing it in the same config.
252
+ - **The php-fpm pool overrides `/etc/php.d/*.ini` — never trust an INI value that fpm can
253
+ override.** `memory_limit` / `display_errors` set in `application.ini` ran as 256M/Off because
254
+ the fpm pool `www.conf` wins at runtime. Values that must hold (e.g. session handler config)
255
+ belong in code via `ini_set()`, or in the fpm pool itself.
256
+ - **The INI-file parser silently drops a `session.save_path` of the form `"seed[]=host:port"`**
257
+ (in both `/etc/php.d/*.ini` and the fpm pool). `rediscluster` handler applies but save_path
258
+ stays `/tmp` → `session_start()` fatals on every request. Set it via `ini_set()` in code (CLI
259
+ `php -d` with the same value works, so it is the INI-file parser, not the value).
260
+ - **Native PECL extensions: use `php8.5-pecl-redis6`, exclude the php8.1 serializer deps, and
261
+ copy the `.so` into the platform `extension_dir`.** A plain `dnf install` pulls `php8.1-common`
262
+ which conflicts with `php-common-8.5.6`; install with
263
+ `--setopt=install_weak_deps=False --exclude=php8.1-*`, then copy the built `.so` into
264
+ `ini_get('extension_dir')` and verify with `php -m`. Same fix owed to the pgsql sibling hook.
188
265
  - **A SAML SSO return-leg "connection timeout" is a missing HTTPS listener, not code. (CONFIRMED + RESOLVED.)**
189
266
  When the gateway's return redirect to `https://<host>/sso` times out in the browser, verify the
190
267
  **EB/ALB HTTPS (443) listener + ACM cert cover the EXACT return hostname before debugging
@@ -196,6 +273,15 @@ cross-instance storage has a local-disk fallback path for dev, where the AWS SDK
196
273
  returns `200` fast. Reusable for any TOGA app behind Elastic Beanstalk.
197
274
 
198
275
  ## Change history
276
+ - 2026-08-14 — Added the phpredis/pgsql prebuild-hook install pattern
277
+ (`.platform/hooks/prebuild/02-install-php-redis.sh`): package `php8.5-pecl-redis6`,
278
+ `--setopt=install_weak_deps=False --exclude=php8.1-*` to avoid the php8.1-common conflict, and
279
+ copy the built `.so` into the platform `extension_dir`; recorded the pgsql sibling as
280
+ known-broken and bypassed via `PGSQL_HOOK_OPTIONAL=1`
281
+ (`003_pgsql_hook_optional.config`, temporary — breaks the Talos KB create flow). Corrected the
282
+ PHP ini section: `rediscluster` session `save_path` (`seed[]=host:port`) is silently dropped by
283
+ the INI-file parser and the fpm pool overrides `/etc/php.d/*.ini`, so session handling moved to
284
+ runtime `ini_set()` in `framework.php`; added a Redis-sessions section. (jcardinal)
199
285
  - 2026-08-03 — Recorded that prod Tools is multi-instance behind an ALB, so `/var/www/cache` is
200
286
  per-instance local disk (not shared) and cross-request state must go to S3 — the root cause of a
201
287
  Preview→Send "job expired"; noted the no-composer-install deploy, `ENVIRONMENT`-based config
@@ -5,7 +5,7 @@ project: Library
5
5
  client: shared
6
6
  type: standard
7
7
  status: active
8
- updated: 2026-08-06
8
+ updated: 2026-08-14
9
9
  owners: [jcardinal, rgirish, mhammontree, ajean]
10
10
  files: []
11
11
  related:
@@ -306,6 +306,26 @@ Two sanctioned patterns, in order of preference:
306
306
 
307
307
  Never leave either one in effect beyond the block that needs it.
308
308
 
309
+ ### Start the session before App_Error's fatal handler — and set the handler in code, not INI
310
+
311
+ On EB PHP 8.5 / AL2023, the `.so` extension and cookie flags aside, two rules make Redis
312
+ sessions reliable:
313
+
314
+ 1. **Set `session.save_handler` / `session.save_path` at runtime with `ini_set()`, not in an
315
+ INI file.** The INI-file parser silently drops a `session.save_path` of the form
316
+ `"seed[]=host:port"` (in both `/etc/php.d/*.ini` and the php-fpm pool), and the fpm pool
317
+ overrides `/etc/php.d/*.ini` anyway. `session_start()` then fatals on every request. `ini_set()`
318
+ in the app framework takes the value verbatim.
319
+ 2. **Call `session_start()` before App_Error's fatal handler is installed.** Because any 1.0
320
+ warning routes through `App_Error::handleError` → `exit` (see above), a session store that is
321
+ unreachable would take the whole site down. Starting the session first lets an unreachable
322
+ store degrade to file sessions via a guarded `@session_start()` fallback (acceptable for a
323
+ graceful boot; file sessions are single-instance only). Set the framework's own
324
+ `useSessions=false` flag so the parent boot does not double-start.
325
+
326
+ Reference implementation: `App_Framework_Tools::startSession()` — see the Tools architecture and
327
+ its EB deploy workflow.
328
+
309
329
  ### Documentation
310
330
 
311
331
  * **DocBlocks:** framework (`library`) classes/methods are well documented with a description and `@param`/`@return`/`@throws`. Application code is less consistent — document new and modified methods to the framework standard.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.586",
3
+ "version": "1.0.588",
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",