toga-ai 1.0.1081 → 1.0.1083
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.
- package/knowledge/2.0/apps/_underscore/INDEX.md +2 -1
- package/knowledge/2.0/apps/_underscore/features/email-template-sending.md +3 -1
- package/knowledge/2.0/apps/_underscore/features/service-request-status-and-stages.md +60 -0
- package/knowledge/2.0/apps/_underscore/features/sla-engine.md +30 -19
- package/knowledge/2.0/apps/api2/features/api-payload-interceptors.md +6 -2
- package/knowledge/2.0/apps/api2/features/v2-rest-query-contract.md +4 -2
- package/knowledge/2.0/apps/dbchanges2/features/rerunnable-additive-inserts.md +3 -1
- package/knowledge/INDEX.md +3 -3
- package/knowledge/standalone/apps/bdr/features/landing-chat-drawer.md +3 -1
- package/knowledge/standalone/apps/talos-backend/INDEX.md +1 -0
- package/knowledge/standalone/apps/talos-backend/features/ai-service-endpoints.md +6 -4
- package/knowledge/standalone/apps/talos-backend/features/website-chat-security.md +62 -0
- package/knowledge/standalone/apps/togatech/INDEX.md +1 -0
- package/knowledge/standalone/apps/togatech/features/talos-chat-widget.md +53 -0
- package/package.json +1 -1
|
@@ -63,8 +63,9 @@
|
|
|
63
63
|
| [SO↔PO and TO↔PO bridge tables are TWO tables in OPPOSITE directions (upstream vs downstream)](features/sales-order-purchase-order-bridge-direction.md) | The two SO↔PO (and two TO↔PO) bridge tables mean opposite things (upstream = customer PO, downstream = vendor PO); open when an SO/TO↔PO query returns an empty |
|
|
64
64
|
| [Per-client sales-order status filter (Surface FILTER_SET → table meta `filterOptions`)](features/sales-order-status-filter-surface.md) | Per-client status-filter dropdown on the sales-orders table, driven by a Surface `FILTER_SET`; open when the offered statuses are wrong for a tenant or when onb |
|
|
65
65
|
| [Service request links (ServiceRequests_Tickets, ServiceRequests_ServiceRequests bridges)](features/service-request-links.md) | Many-to-many links from a service request to tickets and to other service requests; open before linking SRs, reading `_linkCount`, or touching the old `ServiceR |
|
|
66
|
+
| [2.0 Service Request status (`_status`) is derived; stages are manual-only; standard schedule / crew / location columns](features/service-request-status-and-stages.md) | How a 2.0 service request's `_status` is computed, what stages are for, the standard SR columns, and the set-stage write; open when an SR shows the wrong status |
|
|
66
67
|
| [Shipping Carriers & Carrier Account Numbers (records, matching, duplication, deletion)](features/shipping-carriers-and-accounts.md) | Per-client `ShippingCarriers` / `ShippingCarrierAccountNumbers` model, how the syncs match/create carrier rows, and the duplication + blank-carrier + pagination |
|
|
67
|
-
| [
|
|
68
|
+
| [SLA engine (_Component_Sla) — record-generic (tickets + service requests), two clocks, per-stage effects, client switch + operator recompute](features/sla-engine.md) | How 2.0 SLA due times, pauses and the per-client switch are computed and written for tickets AND service requests (`_Component_Sla`); open when an SLA countdown |
|
|
68
69
|
| [_String helpers — ASCII-safe HTML entity encoding (and the parseBetween trap)](features/string-html-entity-helpers.md) | `_String` static string utilities: use `encodeToAsciiHtmlEntities` for any dynamic text going into a 2.0 email; open when escaping email text or parsing substri |
|
|
69
70
|
| [Surface FIELD ↔ RecordField binding (maxLength / validationType / isWritable fold-in)](features/surface-field-recordfield-binding.md) | Resolver capability that folds `Core.RecordFields` input-contract metadata (maxLength/validationType/isWritable) onto a surface FIELD element; open when a 2.5 s |
|
|
70
71
|
| [DECLINED 2026-10-08 — re-key Client.SurfaceOverrides from numeric Core ids to uuids](features/surface-overrides-uuid-rekey.md) | **Decision 2026-10-08 (jcardinal): keep numeric ids; no uuid re-key.** The plan below is not scheduled; see the Layer boundary in [surface-resolver](surface-res |
|
|
@@ -6,7 +6,7 @@ project: _Underscore
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-10-
|
|
9
|
+
updated: 2026-10-19
|
|
10
10
|
owners: ["jcardinal", "bala", "mhammontree", "apeterson"]
|
|
11
11
|
files:
|
|
12
12
|
- _underscore/Model/Client/EmailTemplate.php
|
|
@@ -113,6 +113,7 @@ can keep using `sendEmail($api, ...)`.
|
|
|
113
113
|
|
|
114
114
|
## Gotchas / known issues
|
|
115
115
|
|
|
116
|
+
- **⚠ `email-templates/sendEmail` is called straight from the public togatech.com bundle** (footer form, contact form, Talos chat lead) as a GET with uuid/to/cc/body in the URL, and it checks no captcha. Anyone can replay it with their own recipients. Open follow-up (2026-10-19): verify reCAPTCHA v3 server-side (actions `footer_form_submit`, `contact_form_submit`, `talos_lead`; hostname `togatech.com`; score ≥ 0.5), pin recipients per template and ignore public `to`/`cc`, rate-limit, prefer POST, and **exempt authenticated service callers**, because Talos sends reminder/scheduled-run emails through it server-to-server. Details: [togatech talos-chat-widget](../../../../standalone/apps/togatech/features/talos-chat-widget.md).
|
|
116
117
|
- **⚠ `replaceTemplateVariables()` does NO escaping — the CALLER must escape.** It is a plain
|
|
117
118
|
`str_replace` per key, and the Send worker transmits with `IsHTML(true)`, so any stored value
|
|
118
119
|
holding a `<` (a site name, a contact name, an address line, a PO number) lands in the delivered
|
|
@@ -237,6 +238,7 @@ worker method) in-process instead.
|
|
|
237
238
|
|
|
238
239
|
## Change history
|
|
239
240
|
|
|
241
|
+
- 2026-10-19 — Recorded the public, unverified togatech.com use of sendEmail and the lockdown follow-up. (akhokhani)
|
|
240
242
|
- 2026-10-15 — Recorded the empty-string `to` + cc gap (queued, fails in worker2). (jcardinal)
|
|
241
243
|
- 2026-10-06 — Noted the 1054 symptom when the email-review migrations are missing in an env (client-sandbox). (bala)
|
|
242
244
|
- 2026-09-23 — Added the review hold (`isReviewRequired` + `REVIEW` addresses → `HELD` real email +
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: 2.0 Service Request status (`_status`) is derived; stages are manual-only; standard schedule / crew / location columns
|
|
3
|
+
framework: "2.0"
|
|
4
|
+
repo: _underscore
|
|
5
|
+
project: _Underscore
|
|
6
|
+
client: shared
|
|
7
|
+
type: feature
|
|
8
|
+
status: active
|
|
9
|
+
updated: 2026-10-19
|
|
10
|
+
owners: [jcardinal]
|
|
11
|
+
files:
|
|
12
|
+
- _underscore/Model/Client/ServiceRequest.php
|
|
13
|
+
- _underscore/Model/Client/ServiceRequestNote.php
|
|
14
|
+
- _underscore/Model/Client/User.php
|
|
15
|
+
- _underscore/Model/Client/Case.php
|
|
16
|
+
- _underscore/Model/Prudential/ServiceRequest.php
|
|
17
|
+
- _underscore/Model/Growrk/ServiceRequest.php
|
|
18
|
+
- _underscore/Model/Adyen/ServiceRequest.php
|
|
19
|
+
- _underscore/Model/Elite/ServiceRequest.php
|
|
20
|
+
- dbchanges2/Core/2026-10-19i - TRUE-82915 ServiceRequestInterceptors.sql
|
|
21
|
+
- dbchanges2/Client_Prudential/2026-10-19a - TRUE-82915 ServiceRequestStageStatusLinks.sql
|
|
22
|
+
- dbchanges2/Client_Growrk/2026-10-19a - TRUE-82915 ServiceRequestStageStatusLinks.sql
|
|
23
|
+
related:
|
|
24
|
+
- ./ticket-status-from-stage.md
|
|
25
|
+
- ./case-status-and-stage-gate.md
|
|
26
|
+
- ./sla-engine.md
|
|
27
|
+
- ./service-request-links.md
|
|
28
|
+
- ../../api2/features/api-payload-interceptors.md
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
How a 2.0 service request's `_status` is computed, what stages are for, the standard SR columns, and the set-stage write; open when an SR shows the wrong status, you add a stage, or you add a base SR hook.
|
|
32
|
+
|
|
33
|
+
## Summary
|
|
34
|
+
|
|
35
|
+
`_Model_Client_ServiceRequest::_status` is a `FIELD_SQL` calculated field (same idea as Tickets and Cases). Status comes from the SR's own fields; stages hold only manual states that cannot be derived. Full detail: toga25-desk `docs/decisions/0166-sr-status-derived-from-fields-stages-manual-only.md` and `0167-sr-site-schedule-crew-tags-reported-by.md`.
|
|
36
|
+
|
|
37
|
+
## How it works
|
|
38
|
+
|
|
39
|
+
`_status` returns, in order:
|
|
40
|
+
1. the linked status slug, when the stage has `serviceRequestStatusId`;
|
|
41
|
+
2. `closed`, when the stage is unlinked and `isOpen = 0`;
|
|
42
|
+
3. `in-progress`, when a Lead (`assignedToContactId`) or crew (`ServiceRequests_Users`) is set;
|
|
43
|
+
4. `scheduled`, when `dtScheduledStart` is set;
|
|
44
|
+
5. else `new`.
|
|
45
|
+
|
|
46
|
+
- **Stages = manual, non-derivable states only:** blockers (roadblocked / on-hold / awaiting-user / awaiting-parts — each needs a reason note in `ServiceRequestNotes`), pending-client-approval, resolved, closed. No stage is the normal flow.
|
|
47
|
+
- **Per-client stage → status links** go in `Client_<Name>/` files (e.g. `Client_Prudential` / `Client_Growrk` `2026-10-19a`), never a `Client/` fan-out.
|
|
48
|
+
- **Standard columns:** `assignedToContactId` (Lead), `reportedByContactId`, `locationId` (the site — NOT `deliverToAddressId`, which is the shipping address), `isRemote`, `dtScheduledStart` / `dtScheduledEnd` (NOT `dateDelivery`). `ServiceRequestTypes`: `isSiteVisit`, `iconName`, `colorHex`. Bridges `ServiceRequests_Users` (crew), `_Tags`, `_Files` (Core.Records 422-424, RecordFields 3106-3152).
|
|
49
|
+
- **Set stage** = table-based `PUT /v2/service-requests/{uuid}` with nested `serviceRequestNotes` (the reason note). A stage PUT without a uuid = 400 for signed-in users (CSO).
|
|
50
|
+
- **Crew "required"** is a UI rule only — integration-created SRs send no crew.
|
|
51
|
+
- **Base hooks** (`prePost` / `postPost` / `postPut` / `postPatch`) are registered for every client by `Core/2026-10-19i` (`clientId` / `apiId` NULL). They run the schedule check, the set-stage gate, and the [SLA engine](./sla-engine.md).
|
|
52
|
+
|
|
53
|
+
## Gotchas
|
|
54
|
+
|
|
55
|
+
- **⚠ Never delete or rename existing stage rows.** Client integrations match stage NAMES: Prudential `postPut` ('Cancelled' / 'Update'), Growrk ('Completed'), worker 1.0 cron ('Exception' + stage id 2).
|
|
56
|
+
- **⚠ A client SR override must call `parent::` in every hook it overrides.** Before 2026-10-19 the Prudential / Growrk / Adyen / Elite `ServiceRequest` overrides did not, so every base SR hook was silently skipped for those clients. Adding a base hook? Check each `_Model_<Client>_ServiceRequest` calls `parent::` first (and that the parent declares the method — see [interceptors](../../api2/features/api-payload-interceptors.md#the-client-model-resolution-that-decides-which-class-must-define-the-method)).
|
|
57
|
+
- **⚠ A NULL stage is now the normal case — never INNER JOIN the SR stage.** `User::_openWorkCount` and `Case::_resolvedCount` used INNER JOIN on stage + `isOpen` and dropped stageless SRs. Rule: open = no stage OR stage `isOpen = 1`; open work counts the SR's own crew.
|
|
58
|
+
|
|
59
|
+
## Change history
|
|
60
|
+
- 2026-10-19 — `_status` derived from fields; stages manual-only; schedule / crew / location / reported-by columns; client overrides call `parent::`; open counts include stageless SRs (TRUE-82915). (jcardinal)
|
|
@@ -1,65 +1,76 @@
|
|
|
1
1
|
---
|
|
2
|
-
title:
|
|
2
|
+
title: SLA engine (_Component_Sla) — record-generic (tickets + service requests), two clocks, per-stage effects, client switch + operator recompute
|
|
3
3
|
framework: "2.0"
|
|
4
4
|
repo: _underscore
|
|
5
5
|
project: _Underscore
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-10-
|
|
9
|
+
updated: 2026-10-19
|
|
10
10
|
owners: [jcardinal]
|
|
11
11
|
files:
|
|
12
12
|
- _underscore/Component/Sla/Sla.php
|
|
13
13
|
- _underscore/Model/Client/Ticket.php
|
|
14
14
|
- _underscore/Model/Client/TicketStage.php
|
|
15
15
|
- _underscore/Model/Client/TicketNote.php
|
|
16
|
+
- _underscore/Model/Client/ServiceRequest.php
|
|
17
|
+
- _underscore/Model/Client/ServiceRequestNote.php
|
|
16
18
|
- _underscore/Model/Core/Client.php
|
|
17
19
|
- dbchanges2/Core/2026-10-16a - SlaReplyClockCore.sql
|
|
18
20
|
- dbchanges2/Client/2026-10-16a - SlaReplyClockFanout.sql
|
|
19
21
|
- dbchanges2/Client_True/2026-10-16a - SlaStageDataRepair.sql
|
|
20
|
-
- dbchanges2/
|
|
22
|
+
- dbchanges2/Core/2026-10-19k - TRUE-82915 ServiceRequestSlaCore.sql
|
|
23
|
+
- dbchanges2/Client/2026-10-19i - TRUE-82915 ServiceRequestSla.sql
|
|
21
24
|
related:
|
|
22
25
|
- ./ticket-status-from-stage.md
|
|
26
|
+
- ./service-request-status-and-stages.md
|
|
23
27
|
- ../../api2/features/api-payload-interceptors.md
|
|
24
28
|
- ../../api2/features/tickets-api.md
|
|
25
29
|
- ../../dbchanges2/features/rerunnable-additive-inserts.md
|
|
26
30
|
---
|
|
27
31
|
|
|
28
|
-
How 2.0
|
|
32
|
+
How 2.0 SLA due times, pauses and the per-client switch are computed and written for tickets AND service requests (`_Component_Sla`); open when an SLA countdown is wrong, a stage does not pause, you need to recompute a client, or you want SLA on another record type.
|
|
29
33
|
|
|
30
34
|
## Summary
|
|
31
35
|
|
|
32
|
-
`_Component_Sla` (`_underscore/Component/Sla/Sla.php`) is the ONLY code that writes `
|
|
36
|
+
`_Component_Sla` (`_underscore/Component/Sla/Sla.php`) is the ONLY code that writes the stored SLA columns (`sla*` / `dtSla*` / `dtFirstResponse`). One engine, record-generic: every record-specific name comes from `_Component_Sla::descriptor($record)`. Static, shared by every 2.0 app, called from api2 payload interceptors (not model save hooks). Two clocks — resolution and reply (first response) — each with its own pause bookkeeping, driven by per-stage effects stored as client data.
|
|
33
37
|
|
|
34
38
|
## How it works
|
|
35
39
|
|
|
36
|
-
- **
|
|
40
|
+
- **Descriptor:** `descriptor(RECORD_TICKET | RECORD_SERVICE_REQUEST)` (`'ticket'` / `'serviceRequest'`) returns `record, label, table, typeColumn, stageColumn, contactColumn, targetsTable, stagesTable, hasReplyEffectColumn, notesTable, noteRecordColumn, noteStageColumn, noteAuthorUserColumn`. `allRecordTypes()` = what the client switch and recompute cover. **New record type = a new descriptor + the same columns — never a copy of the engine** (decided, CTO). Detail: toga25-desk `docs/decisions/0168-sla-engine-record-generic.md`.
|
|
41
|
+
- **Tickets (descriptor 1):** `TicketSlas`, `TicketStages.slaEffect` + `slaFirstResponseEffect`, `TicketNotes`, contact = `contactId`. Moving tickets onto the descriptor changed nothing (275-scenario old-vs-new harness and a live recompute replay: 0 diffs).
|
|
42
|
+
- **Service requests (descriptor 2):** same 9 stored SLA columns + 11 calc fields on `ServiceRequests`; targets `ServiceRequestSlas`; effect `ServiceRequestStages.slaEffect`. No reply-effect column (`hasReplyEffectColumn = false`) — the reply clock follows `slaEffect`. Contact = `requestedForContactId`. First response = first external note with a signed-in author (`authorUserId`) OR a contact other than the requester.
|
|
43
|
+
- **Targets:** `(typeColumn, urgencyId)` → minutes (MINUTES ×1, HOURS ×60, DAYS ×1440). Any other unit (e.g. `BUSINESS_DAYS`) = no target. No business-day math.
|
|
37
44
|
- **Due:** `dtCreated + targetMinutes + pausedMinutes`, per clock.
|
|
38
|
-
- **Resolution clock:**
|
|
39
|
-
- **Reply clock:**
|
|
40
|
-
- **`resolvePauseState(params)`** is clock-agnostic
|
|
41
|
-
- **
|
|
42
|
-
- **Read side:** `_slaTime`, `_slaState`, `_slaPausedAt` (FIELD_SQL on
|
|
45
|
+
- **Resolution clock:** stage `slaEffect` (`RUN`/`PAUSE`/`STOP`, no stage = RUN). Columns `dtSlaPaused`, `slaPausedMinutes`. STOP time is charged like pause time; `_slaState` shows `STOPPED`.
|
|
46
|
+
- **Reply clock (tickets):** `TicketStages.slaFirstResponseEffect` (`RUN`/`PAUSE`, column default `PAUSE`, no stage = RUN). Columns `dtSlaFirstResponsePaused`, `slaFirstResponsePausedMinutes`. Seeded RUN only for new / open / reopened stages. Keeps tracking after the first response so stamps stay right.
|
|
47
|
+
- **`resolvePauseState(params)`** is clock-agnostic, called once per clock. Do not fork the math.
|
|
48
|
+
- **Ticket first response** = `MIN(TicketNotes.dtStamp)` of external notes by a contact other than the ticket's own contact. Authorless notes never count.
|
|
49
|
+
- **Read side:** `_slaTime`, `_slaState`, `_slaPausedAt` (FIELD_SQL on the record model). `_slaPausedAt` returns the paused stamp of the clock being shown (reply while unanswered and a reply due exists, else resolution). UIs read it, not the raw columns.
|
|
43
50
|
- **Time = DB clock** (`SELECT NOW()` on the client writer), read once, then advanced by PHP elapsed time.
|
|
44
|
-
- **Client switch:** `Core.Clients.isSlaEnabled
|
|
45
|
-
- **Operator recompute (no OFF/ON):** `PUT /v2/clients/{uuid}` body `{"recomputeSla": true}
|
|
46
|
-
- **One run per client:** the
|
|
47
|
-
- **Cross-client:**
|
|
51
|
+
- **Client switch:** `Core.Clients.isSlaEnabled` covers every record type. OFF = every SLA column NULL. Flipping it in `PUT /v2/clients/{uuid}` runs `applyClientSwitch` (backfill, skips STOP-stage records when turning on).
|
|
52
|
+
- **Operator recompute (no OFF/ON):** `PUT /v2/clients/{uuid}` body `{"recomputeSla": true}` — all record types. `_Model_Core_Client::prePut` strips the key (not a column), requires boolean `true`, parks it on `$api->passThrough`; `postPut` runs `recomputeClient` (STOP included). Allowed only for callers who can WRITE `Clients.isSlaEnabled`. A body empty after the strip becomes `{}` in V2.php and `postPut` still runs. Pattern: [command keys in interceptors](../../api2/features/api-payload-interceptors.md#passing-state-from-a-pre-hook-to-a-post-hook).
|
|
53
|
+
- **One run per client:** the backfill takes `GET_LOCK('sla-recompute-<clientId>', 0)` on the SAME connection as its writes, `RELEASE_LOCK` in `finally`. Lock busy → `_Exception_Validation` 400. Any failure throws, so the API write rolls back. Reuse this shape for any "one at a time per client" operation.
|
|
54
|
+
- **Cross-client:** switch/recompute use `DB_TARGET_CLIENT` (`registerTargetClientDatabase`), never the caller's `DB_CLIENT`. Core and client data are never joined.
|
|
48
55
|
|
|
49
56
|
## Gotchas
|
|
50
57
|
|
|
51
|
-
- **⚠ A stage that "never pauses" is usually data, not code.** Check `
|
|
58
|
+
- **⚠ A stage that "never pauses" is usually data, not code.** Check the stage table's `slaEffect` (and `slaFirstResponseEffect` for tickets) in that environment first. QA alpha `Client_True` (2026-10-16): every ticket stage stayed RUN because the `Client/2026-09-29d` seed matched by slug before the slugs existed; fixed by `Client_True/2026-10-16a - SlaStageDataRepair.sql`. See [fan-out seed keyed on a slug](../../dbchanges2/features/rerunnable-additive-inserts.md#gotchas).
|
|
59
|
+
- **⚠ Writes that skip api2 skip the SLA.** worker 1.0 Prudential crons set `ServiceRequests.serviceRequestStageId = 2` by direct SQL — no pause is recorded until the next api2 write or a recompute. Accepted gap.
|
|
60
|
+
- **⚠ Full recompute resets accrued paused minutes on records no longer paused** (both engines, pre-existing). Know this before running `recomputeSla` on a client with long past pauses.
|
|
52
61
|
- **⚠ The lock is released before api2 commits.** A second recompute started right after can overlap the first's uncommitted writes. Accepted (operator-only, rare) — do not copy this gap into a high-traffic operation; hold the lock past commit there.
|
|
53
|
-
- **No worker job for big clients** (decided).
|
|
54
|
-
- **`FIELD_LIST` does not validate ENUMs** — `TicketStage::save()` checks both effects explicitly. Keep that check when adding a value.
|
|
55
|
-
- **Changing a stage's effect does not move existing
|
|
62
|
+
- **No worker job for big clients** (decided). Recompute runs inside the request; a very large client can hit the 300 s request cap. Paged at 500 records.
|
|
63
|
+
- **`FIELD_LIST` does not validate ENUMs** — `TicketStage::save()` checks both effects explicitly. Keep that check (and add it on any new stage model) when adding a value.
|
|
64
|
+
- **Changing a stage's effect does not move existing records** until their next write or a recompute. Run `{"recomputeSla": true}` after a stage-effect data fix.
|
|
56
65
|
|
|
57
66
|
## Related
|
|
58
67
|
|
|
59
68
|
- [Ticket status from stage](./ticket-status-from-stage.md)
|
|
69
|
+
- [Service request status and stages](./service-request-status-and-stages.md)
|
|
60
70
|
- [API payload interceptors](../../api2/features/api-payload-interceptors.md)
|
|
61
71
|
- [Tickets API](../../api2/features/tickets-api.md)
|
|
62
72
|
- [Re-runnable additive migrations](../../dbchanges2/features/rerunnable-additive-inserts.md)
|
|
63
73
|
|
|
64
74
|
## Change history
|
|
75
|
+
- 2026-10-19 — Engine made record-generic via `descriptor()`; service requests are descriptor 2 (same columns, `ServiceRequestSlas`, reply clock follows `slaEffect`); tickets unchanged (0 diffs) (TRUE-82915). (jcardinal)
|
|
65
76
|
- 2026-10-16 — Reply clock gets its own per-stage effect (`slaFirstResponseEffect`, pauses outside new/open/reopened); operator recompute via `{"recomputeSla": true}`; per-client named lock (TRUE-82849). (jcardinal)
|
|
@@ -6,7 +6,7 @@ project: API
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-10-
|
|
9
|
+
updated: 2026-10-19
|
|
10
10
|
owners: ["mhammontree", "dfranks", "bala", "snaredla", "jcardinal", "rgirish"]
|
|
11
11
|
files:
|
|
12
12
|
- api2/Component/Api/V2/V2.php
|
|
@@ -50,6 +50,7 @@ related:
|
|
|
50
50
|
- ./language-translation-layer.md
|
|
51
51
|
- ../../_underscore/features/persona-nav-visibility-gate.md
|
|
52
52
|
- ../../../clients/compass-usa/features/approval-decision-flow.md
|
|
53
|
+
- ../../_underscore/features/service-request-status-and-stages.md
|
|
53
54
|
---
|
|
54
55
|
|
|
55
56
|
How `_Model` prePost/postPost/prePut/postPut hooks are dispatched by the api2 /v2 engine only when a metadata row exists — the missing-row silent no-op, the wrong-row hard fatal, and the persistence/transaction traps; open before writing, activating, or debugging any interceptor.
|
|
@@ -343,7 +344,8 @@ Refuse API-key callers when the rule is "the signed-in user" (no user on the tok
|
|
|
343
344
|
- **⚠ Client behaviour never goes on `_Model_Client_X`.** Every tenant with a row for that record inherits it through `parent::` calls. Put it on `_Model_<Client>_X`.
|
|
344
345
|
- **⚠ An own-row PRE guard that checks only `userId` is bypassable** — alias keys (`user`) map after PRE, and a parent-route POST overwrites the stamped FK. Allowlist keys + refuse parent routes. See [own-row guards](#own-row-guards-in-a-pre-interceptor).
|
|
345
346
|
- **A client's rows may be in `Core` (with `clientId`), not in `Client_<X>`** — check both before concluding a client has no interceptors.
|
|
346
|
-
-
|
|
347
|
+
- **Duplicate rows are de-duplicated (since 2026-10-19, TRUE-82915).** `registerApiPayloadInterceptorLookupEntry` (`V2.php`) keeps ONE entry per `(record, PRE|POST, httpMethod)` across Core and client tables; the highest `minDepth` wins (NULL lowest). So a generic row (`clientId` / `apiId` NULL) plus a client's own row dispatches once. Before this, both fired (Adyen 2026-10-08: double worker jobs; Elite / Adyen SR `postPost` → double sales-order tasks). **Rollback order:** deactivate generic rows that overlap client rows BEFORE rolling api2 back, or the double dispatch returns. Pre-release check query: header of `dbchanges2/Core/2026-10-19i - TRUE-82915 ServiceRequestInterceptors.sql`. Still keep each key in one table.
|
|
348
|
+
- **⚠ A client override that skips `parent::` silently disables every base hook for that client.** Found 2026-10-19 on four `ServiceRequest` overrides — see [SR status and stages](../../_underscore/features/service-request-status-and-stages.md#gotchas). Adding a base hook = check every client subclass calls `parent::`.
|
|
347
349
|
- **The method name is derived, so a typo'd enum silently misses.** A row with `prePostProcessing = 'PRE'`, `httpMethod = 'PUT'` resolves to `prePut`, not `prePost`; the engine finds no method and moves on.
|
|
348
350
|
- **Registration is per-environment.** Like `RecordFields` / `RecordScripts` / ACL rows, this is metadata — "works in prod, not in beta/dev-sandbox" is the signature of drift, not of a bad deploy. See the [non-prod metadata drift repair workflow](../../dbchanges2/workflows/nonprod-metadata-drift-repair.md).
|
|
349
351
|
- **⚠ An in-request read-after-write in an interceptor works on beta and fails 100% in production.** Reads default to the **reader** connection, which cannot see the request's own uncommitted row; dev-sandbox points writer and reader at the same endpoint, so it hides the bug. Pin with `setIsReadHostEnabled(false)` (restore in `finally`).
|
|
@@ -368,6 +370,7 @@ Refuse API-key callers when the rule is "the signed-in user" (no user on the tok
|
|
|
368
370
|
- [Email template sending](../../_underscore/features/email-template-sending.md)
|
|
369
371
|
- [Elite SalesOrder → NetSuite push](../../../clients/elite/features/salesorder-netsuite-push.md)
|
|
370
372
|
- [Compass sales-order line renumbering](../../../clients/compass-usa/features/sales-order-line-renumbering.md)
|
|
373
|
+
- [Service request status and stages](../../_underscore/features/service-request-status-and-stages.md)
|
|
371
374
|
|
|
372
375
|
## Change history
|
|
373
376
|
- 2026-10-08 — `internalApiRequest` "[]" throw also fires on the OUTER request's warnings (e.g. WO-1); throw site now ~L2436; Core + client duplicate rows fire twice. (rgirish)
|
|
@@ -376,3 +379,4 @@ Refuse API-key callers when the rule is "the signed-in user" (no user on the tok
|
|
|
376
379
|
- 2026-10-16 — Command keys: strip in `prePut`, park on `passThrough`, act in `postPut`; empty body → `{}` (TRUE-82849). (jcardinal)
|
|
377
380
|
- 2026-10-18 — Write intent that writes another record via internal request (caller ACL, rollback, `addNote` after the call) (TRUE-82876). (jcardinal)
|
|
378
381
|
- 2026-10-17 — "Action table" pattern for audited actions; write intents before a stage gate; new interceptor rows ship in a file run after the code deploy (TRUE-82889). (jcardinal)
|
|
382
|
+
- 2026-10-19 — Lookup de-duplicates rows per record / PRE|POST / method (highest `minDepth` wins); client overrides must call `parent::` (TRUE-82915). (jcardinal)
|
|
@@ -49,7 +49,9 @@ controller.
|
|
|
49
49
|
`fields`, `where`, `join` / `ojoin`, `sort`, `group`, `distinct`, `page`, `recordsPerPage`,
|
|
50
50
|
`depth`, `calcDepth` (or one base64(JSON) `_` blob).
|
|
51
51
|
|
|
52
|
-
- **Pagination is `page` + `recordsPerPage`. There is no `limit`/`offset`.**
|
|
52
|
+
- **Pagination is `page` + `recordsPerPage`. There is no `limit`/`offset`.** A `limit=5` on a
|
|
53
|
+
list GET is not ignored — it returns **404 `EV-14`** "Expecting one record ... multiple records
|
|
54
|
+
were found". Defaults:
|
|
53
55
|
`recordsPerPage` **25**, `depth` **3**. Max `recordsPerPage` **10000** — exceeding it returns
|
|
54
56
|
**`EV-13`** (HTTP 400).
|
|
55
57
|
- **Sort:** `-field` = DESC, `+field` = ASC, bare `field` = ASC. All three are handled
|
|
@@ -656,6 +658,7 @@ If you need the behaviour, loop uuids client-side. Point 3 is the durable, non-o
|
|
|
656
658
|
[V2 request logging](request-logging.md).
|
|
657
659
|
|
|
658
660
|
## Change history
|
|
661
|
+
- 2026-10-19 — `limit` on a list GET = 404 `EV-14`, use `recordsPerPage` (TRUE-82915). (jcardinal)
|
|
659
662
|
- 2026-10-19 — Self-join `where` needs an `ojoin` alias (`Locations@p`); `_` field ACL bypass does not apply cross-client (TRUE-82911). (jcardinal)
|
|
660
663
|
- 2026-10-18 — Grouped queries: stored root group key now selected + ACL-gated; known gaps (joined key, total count, `ORDER BY uuid` under ONLY_FULL_GROUP_BY) (TRUE-82876). (jcardinal)
|
|
661
664
|
- 2026-10-17 — Child-path `where` must start at an inherent child; FK-first path → TableView join alias (TRUE-82872). (jcardinal)
|
|
@@ -667,7 +670,6 @@ If you need the behaviour, loop uuids client-side. Point 3 is the durable, non-o
|
|
|
667
670
|
- 2026-09-18 — `in` instead of an OR-chain of `contains` shortens SQL only, not speed. (apeterson)
|
|
668
671
|
- 2026-09-17 — Bulk DELETE by `where`: exists, unused, unsafe; zero matches looks like an ACL denial. (rgirish)
|
|
669
672
|
- 2026-09-14 — `where` on a calculated field works with the bare name; `calcDepth` does not gate a named calc field; list GET cost model. (apeterson)
|
|
670
|
-
- 2026-09-08 — `depth` read-back cost (~830s at `depth=5` on a write). (jcardinal)
|
|
671
673
|
- 2026-09-04 — URL-encoded `where` → 500/`EO-1` (`Logs.Issue` 97). (rgirish)
|
|
672
674
|
- 2026-09-01/02 — `_` field in `fields=` is never ACL-checked; inherent-child keys govern writes; bridge `InherentRecordChildren` rows. (apeterson, bala)
|
|
673
675
|
- 2026-07-29 → 08-30 — Created; then: `fields=` does not narrow SELECT, `depth` races deletes, sort `+`/DISTINCT, uuid-READ ignores `where`, joins drive filter not shape, `calcDepth` default 1, FK omitted at max depth, PUT echo can omit fields. (tcox, bala, apeterson, jcardinal)
|
|
@@ -6,7 +6,7 @@ project: Database Changes
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-10-
|
|
9
|
+
updated: 2026-10-19
|
|
10
10
|
owners: [bala, kyalamarthi, apeterson, ajean, jcardinal, rgirish]
|
|
11
11
|
files:
|
|
12
12
|
- dbchanges2/Client_Compass/2026-08-26a - CompassCreativeStudioPersona.sql
|
|
@@ -491,6 +491,7 @@ an INSERT that names only `uuid`/`tableViewId`/`recordFieldId`/`isVisible`/`inde
|
|
|
491
491
|
- **⚠ `NOT NULL DEFAULT <value>` does not save a `_Model` column** — the ORM writes an explicit NULL,
|
|
492
492
|
which skips the DEFAULT and errors 1048. Ship a model default with the ALTER.
|
|
493
493
|
- **⚠ `col = 0` never matches a NULL** — guard flag-enabling UPDATEs with `COALESCE(col, 0) = 0`, or the migration reports success and changes nothing.
|
|
494
|
+
- **⚠ A `WHERE col <> 'New text'` guard SKIPS a case-only change on a `_ci` column.** `'service request' <> 'Service Request'` is FALSE under `utf8mb4_general_ci`, so the copy UPDATE runs and changes nothing. Guard copy updates with `BINARY col <> 'New text'`. Bug `Core/2026-10-19j`; fixed in `Core/2026-10-19n`..`p` (TRUE-82915).
|
|
494
495
|
- **⚠ An unguarded ACL grant migration duplicates silently — `AclRecordPermissions` has no unique key
|
|
495
496
|
that stops it (found in prod 2026-09-15).** `_modules/netsuite/2026-09-11c - ItemClassRecordPermission.sql`
|
|
496
497
|
is a plain `INSERT … VALUES` for `(recordId 105, roleId 3)` with **no `NOT EXISTS` guard**. Every
|
|
@@ -560,6 +561,7 @@ an INSERT that names only `uuid`/`tableViewId`/`recordFieldId`/`isVisible`/`inde
|
|
|
560
561
|
[FIELD_SQL calculated fields](../../_underscore/features/calculated-sql-fields.md).
|
|
561
562
|
|
|
562
563
|
## Change history
|
|
564
|
+
- 2026-10-19 — `<>` copy-update guard skips a case-only change on a `_ci` column; use `BINARY` (TRUE-82915). (jcardinal)
|
|
563
565
|
- 2026-10-17 — `TicketTypes.code` has no UNIQUE and may hold duplicates; guard seeds on `code` (TRUE-82889). (jcardinal)
|
|
564
566
|
- 2026-10-16 — Backfill an api2-identifier column before the deploy (seed file), stragglers + ALTER after (TRUE-82841). (jcardinal)
|
|
565
567
|
- 2026-10-16 — NOT NULL alternative for big tables: strict session + `ALGORITHM=INPLACE, LOCK=NONE`
|
package/knowledge/INDEX.md
CHANGED
|
@@ -21,7 +21,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
|
|
|
21
21
|
|
|
22
22
|
## 2.0 framework
|
|
23
23
|
|
|
24
|
-
- **_underscore** (_Underscore) _(framework core)_ —
|
|
24
|
+
- **_underscore** (_Underscore) _(framework core)_ — 111 doc(s) → [2.0/apps/_underscore/INDEX.md](2.0/apps/_underscore/INDEX.md)
|
|
25
25
|
- **worker2** (Worker) — 91 doc(s) → [2.0/apps/worker2/INDEX.md](2.0/apps/worker2/INDEX.md)
|
|
26
26
|
- **api2** (API) — 27 doc(s) → [2.0/apps/api2/INDEX.md](2.0/apps/api2/INDEX.md)
|
|
27
27
|
- **dbchanges2** (Database Changes) _(framework core)_ — 22 doc(s) → [2.0/apps/dbchanges2/INDEX.md](2.0/apps/dbchanges2/INDEX.md)
|
|
@@ -39,10 +39,10 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
|
|
|
39
39
|
|
|
40
40
|
## standalone framework
|
|
41
41
|
|
|
42
|
-
- **togatech** (TOGA Technology Website) —
|
|
42
|
+
- **togatech** (TOGA Technology Website) — 11 doc(s) → [standalone/apps/togatech/INDEX.md](standalone/apps/togatech/INDEX.md)
|
|
43
43
|
- **websocket** (WebSocket Server) — 2 doc(s) → [standalone/apps/websocket/INDEX.md](standalone/apps/websocket/INDEX.md)
|
|
44
44
|
- **talos** (TOGa Talos Chat) — 3 doc(s) → [standalone/apps/talos/INDEX.md](standalone/apps/talos/INDEX.md)
|
|
45
|
-
- **talos-backend** (TOGa Talos) —
|
|
45
|
+
- **talos-backend** (TOGa Talos) — 33 doc(s) → [standalone/apps/talos-backend/INDEX.md](standalone/apps/talos-backend/INDEX.md)
|
|
46
46
|
- **voice-to-voice** (TOGa Voice) — 6 doc(s) → [standalone/apps/voice-to-voice/INDEX.md](standalone/apps/voice-to-voice/INDEX.md)
|
|
47
47
|
- **ai-bdr** (AI-BDR) — 5 doc(s) → [standalone/apps/ai-bdr/INDEX.md](standalone/apps/ai-bdr/INDEX.md)
|
|
48
48
|
- **forward** (Forwarder) — 3 doc(s) → [standalone/apps/forward/INDEX.md](standalone/apps/forward/INDEX.md)
|
|
@@ -6,7 +6,7 @@ project: BDR
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-10-
|
|
9
|
+
updated: 2026-10-19
|
|
10
10
|
owners: [tcox, akhokhani]
|
|
11
11
|
files:
|
|
12
12
|
- bdr/src/components/TalosDrawer.tsx
|
|
@@ -110,6 +110,7 @@ The `.chatveil` / `.chatpanel` block was ported from the security mockup's `styl
|
|
|
110
110
|
- **The drawer runs on a LINKED blox checkout — it is not deployable.** `node_modules/@agilant/toga-blox` is a junction to the local `toga-blox-npm` working tree on an **unmerged, unpublished** branch. A build on any other machine (or Amplify) has no Talos. Do not treat a green local `next build` as evidence the sandbox will build.
|
|
111
111
|
- **A portaled panel does not inherit `.security-shell`.** See *Styling* — theme values must be literals inside `TalosDrawer`, and anything that needs `security.css` rules has to sit inside a `.security-shell` wrapper with `display: contents`.
|
|
112
112
|
- **`api.togaiq.com` chat endpoints are origin/UA-gated — verified 2026-08-25 with browser-shaped `curl` probes:** from origin `http://localhost:3000` the whole flow works end-to-end (token minted, real Sonnet reply with `suggestions`/`cta`/`cited_slides`), so **local dev gets live answers**. From origin `https://bdr.dev.sandbox.togatech.com` the session mint returns **`403 Forbidden`** — the sandbox origin is **not allowlisted**, so the deployed drawer silently answers from the scripted fallback and looks like it works. The eventual production domain will need the same. **Fix = the Talos backend owner adds the origin to the talos-chat allowlist (aegra-api); no BDR change.** CORS is not the problem (preflight returns `access-control-allow-origin: *` with `authorization,content-type` allowed; one 502 on a preflight was transient). Diagnose from the browser console: `talos-chat unavailable, using scripted fallback: session mint failed: 403` = origin not allowlisted; `...: Failed to fetch` = a CORS/preflight failure. Check for suggestion chips before concluding the backend is live.
|
|
113
|
+
- **`talos-session` now checks a reCAPTCHA v3 token** (2026-10-19, see [talos-backend website-chat-security](../../talos-backend/features/website-chat-security.md)). The drawer sends none, so in an env set to `TALOS_CHAT_RECAPTCHA_MODE=enforce` (beta today; prod is `monitor`) the mint returns 403 `missing_token` and the drawer silently falls back to scripted answers. Its domain must also be a `togatech.com` host to pass the hostname check.
|
|
113
114
|
- **Do not "fix" the transcript by sending the visible message list.** The greeting and any merged/trimmed turns would break the strict alternating shape and return a 422.
|
|
114
115
|
- **The chat is fully client-side**, which is why `/landing` still builds as a **prerendered static** route. Adding a server call here changes that.
|
|
115
116
|
- **`/landing` is not rate-limited** (`src/proxy.ts` matches `/` and the API routes, not the page). The drawer talks to a third-party origin, not a BDR API route, so the limiter still does not apply to *chat* — but the abuse surface exists on the page. The drawer's new "Call me now" action does hit a BDR route, and that one **is** limited (`/api/landing-call`, on the `/api/call-now` bucket).
|
|
@@ -125,4 +126,5 @@ The `.chatveil` / `.chatpanel` block was ported from the security mockup's `styl
|
|
|
125
126
|
|
|
126
127
|
## Change history
|
|
127
128
|
|
|
129
|
+
- 2026-10-19 — noted the reCAPTCHA v3 requirement on talos-session (akhokhani)
|
|
128
130
|
- 2026-10-07 — moved to standalone/bdr (framework axis per CONVENTIONS) (akhokhani)
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
| [Tenant configuration — control plane, assistants, personas, system_config](features/tenant-configuration.md) | Where every piece of per-tenant and per-assistant Talos configuration lives (control plane, assistant rows, personas, junction tables, system_config, prompt tem |
|
|
25
25
|
| [TOGa Platform MCP — user-authorized TOGa Supply and Commerce reads over api2](features/toga-platform-mcp.md) | The MCP server that lets the Talos `toga-supply` assistant read TOGa Supply (and, when approved, TOGa Commerce) data as the signed-in user, with api2 enforcing |
|
|
26
26
|
| [Usage dashboard API (/api/usage) and the usage cache jobs](features/usage-dashboard-api.md) | The server-to-server usage analytics API behind the TOGA tools admin usage dashboard (engagement, tokens, cost, Excel exports) and the out-of-process jobs that |
|
|
27
|
+
| [Website chat security — reCAPTCHA v3 session mint, replay guard, one lead per session, rate limits](features/website-chat-security.md) | Abuse controls on the public website chat (`POST /api/ai/talos-session` → `POST /api/ai/talos-chat`): Google reCAPTCHA v3 on session mint, token replay guard, o |
|
|
27
28
|
| [Authoring a BLP — write, lint, upload, bind and verify a Business Logic Protocol file](workflows/authoring-a-blp.md) | The complete guide to writing a Talos BLP file that the parser, the API and the Deep Agent actually honor, then uploading, binding and verifying it; open whenev |
|
|
28
29
|
| [Debugging agent behavior — symptom → cause → fix cookbook for the Deep Agent](workflows/debugging-agent-behavior.md) | How to find out why the Talos Deep Agent answered wrongly, ignored the knowledge base or business logic, lost tools, looped, froze or failed a run, with a sympt |
|
|
29
30
|
| [Debugging Talos in production with Claude Code and the AWS CLI](workflows/debugging-production.md) | Read-only triage procedure for a broken Talos environment (API down, runs stuck, deploy stuck, errors for one client) using Claude Code with the AWS CLI; open w |
|
|
@@ -6,7 +6,7 @@ project: TOGa Talos
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-10-
|
|
9
|
+
updated: 2026-10-19
|
|
10
10
|
owners: [akhokhani]
|
|
11
11
|
files:
|
|
12
12
|
- talos-backend/libs/aegra-api/src/aegra_api/api/ai_generate.py
|
|
@@ -23,6 +23,7 @@ related:
|
|
|
23
23
|
- auth-and-access-control.md
|
|
24
24
|
- client-integration-contract.md
|
|
25
25
|
- bedrock-models-and-cost-attribution.md
|
|
26
|
+
- website-chat-security.md
|
|
26
27
|
- ../../../../2.0/apps/_underscore/features/togaiq-gateway-client.md
|
|
27
28
|
- ../../../../2.0/apps/worker2/features/clickup-task-fluffer-talos.md
|
|
28
29
|
- ../../togatech/features/talos-product-page.md
|
|
@@ -44,7 +45,7 @@ Besides the Agent Protocol chat surface, Talos exposes service endpoints under `
|
|
|
44
45
|
| `POST /api/ai/agent` | `X-API-Key` with the assistant slug in `allowed_model_slugs` (fail-closed 403) | Runs the **full Deep Agent graph** for `assistant_slug` with `messages`, optional `thread_id`, `tool_categories`; `stream:true` → SSE, otherwise sync with a 300 s timeout (504 "Agent run did not complete before the timeout"); `format` = `json` (default) / `markdown` / `text` for the sync response | worker2 `Worker/Clickup/Fluffer.php` (ClickUp task "fluffer", `stream:false`, runs of 3–4 min); config in worker2 `Config/*.ini` |
|
|
45
46
|
| `POST /api/ai/chat` | `X-API-Key` | Direct Bedrock Converse chat with a caller-supplied model (tenant AIP enforced), no tools, no graph | No live caller found (2026-10-07); a stale Cypress env in `togatech` references it |
|
|
46
47
|
| `POST /api/ai/talos-session` | Public, browser-gated | Mints an HS256 session JWT (`TALOS_CHAT_JWT_SECRET`, TTL `TALOS_CHAT_SESSION_TTL_SECONDS` 900 s) bound to IP, UA and a client `fingerprint` | togatech.com website widget (`togatech` `src/lib/talosSession.ts`), BDR landing chat drawer |
|
|
47
|
-
| `POST /api/ai/talos-chat` | `Bearer <session JWT>` + browser gate | Direct Bedrock reply (`TALOS_CHAT_MODEL` = Sonnet 4.6, billed to `TALOS_CHAT_ORG_ID` = `True`), returns `message` plus structured `data` (suggestion chips, CTA, cited slides, lead signal). Optional `campaign_id` grounds the answer in that campaign's collateral. Redis rate limits per IP, per fingerprint and global (`TALOS_CHAT_GLOBAL_MAX_PER_MIN/DAY`)
|
|
48
|
+
| `POST /api/ai/talos-chat` | `Bearer <session JWT>` + browser gate | Direct Bedrock reply (`TALOS_CHAT_MODEL` = Sonnet 4.6, billed to `TALOS_CHAT_ORG_ID` = `True`), returns `message` plus structured `data` (suggestion chips, CTA, cited slides, lead signal). Optional `campaign_id` grounds the answer in that campaign's collateral. Redis rate limits per IP, per fingerprint and global (`TALOS_CHAT_GLOBAL_MAX_PER_MIN/DAY`); reCAPTCHA, replay and lead rules in [website-chat-security](website-chat-security.md) | togatech.com, BDR landing pages |
|
|
48
49
|
| `POST /api/ai/campaigns` | User or API key with `campaign:manage` | Ingests a campaign deck (PDF/images) into `campaigns`, `campaign_documents`, `campaign_document_pages` and registers it in control-plane `campaign_registry` (so a public `campaign_id` resolves its tenant) | Operators/scripts (`scripts/create_campaign_api_key.py`, `scripts/seed_campaign.py`); API key `campaign-seed` in `True` |
|
|
49
50
|
| `GET /api/ai/campaign/{campaign_id}` | Same browser gate as talos-chat | Returns the deck for display (no LLM) | BDR landing pages |
|
|
50
51
|
|
|
@@ -59,13 +60,14 @@ Every talos-session/talos-chat/campaign-read request must have: an `Origin` matc
|
|
|
59
60
|
- `/api/ai/agent` sync calls hold the HTTP request up to 300 s; callers need longer client timeouts than that, or use `stream:true`.
|
|
60
61
|
- `/api/ai/generate` is admission-limited per tenant; bursts from a worker2 batch see 429s — honor `Retry-After`.
|
|
61
62
|
- Repo `docs/talos-chat-campaign-context.md` describes a `campaign_context` request field that no longer exists; the current field is `campaign_id`.
|
|
62
|
-
- **Per-IP
|
|
63
|
+
- **Per-IP limits are still effectively lifted** in prod/beta (2026-10-19): `TALOS_CHAT_TRUSTED_PROXY_COUNT` is unset behind EB nginx + ALB, so all visitors share one peer IP. Limits are now settings and globals are back to 300/min, 10,000/day; reCAPTCHA v3 on session mint, the replay guard and one-lead-per-session live in [website-chat-security](website-chat-security.md).
|
|
63
64
|
- A blocked website origin fails silently in the BDR drawer (it falls back to scripted answers); look for "session mint failed: 403" in the browser console.
|
|
64
65
|
|
|
65
66
|
## Related
|
|
66
|
-
- [auth-and-access-control](auth-and-access-control.md) · [client-integration-contract](client-integration-contract.md) · [bedrock-models-and-cost-attribution](bedrock-models-and-cost-attribution.md)
|
|
67
|
+
- [website-chat-security](website-chat-security.md) · [auth-and-access-control](auth-and-access-control.md) · [client-integration-contract](client-integration-contract.md) · [bedrock-models-and-cost-attribution](bedrock-models-and-cost-attribution.md)
|
|
67
68
|
- Callers: [_underscore togaiq-gateway-client](../../../../2.0/apps/_underscore/features/togaiq-gateway-client.md) · [worker2 clickup-task-fluffer-talos](../../../../2.0/apps/worker2/features/clickup-task-fluffer-talos.md) · [togatech talos-product-page](../../togatech/features/talos-product-page.md) · [bdr campaign-chat-handoff](../../bdr/features/campaign-chat-handoff.md)
|
|
68
69
|
|
|
69
70
|
## Change history
|
|
71
|
+
- 2026-10-19 — rate-limit gotcha updated (limits now configurable, globals restored); security detail moved to website-chat-security (akhokhani)
|
|
70
72
|
- 2026-10-08 — corrected cross-framework/client links after standalone migration (akhokhani)
|
|
71
73
|
- 2026-10-07 — created: endpoint/auth/caller map for `/api/ai/*`; website chat gate verified in code (wildcard matches any subdomain depth) (akhokhani)
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Website chat security — reCAPTCHA v3 session mint, replay guard, one lead per session, rate limits
|
|
3
|
+
framework: "standalone"
|
|
4
|
+
repo: talos-backend
|
|
5
|
+
project: TOGa Talos
|
|
6
|
+
client: shared
|
|
7
|
+
type: feature
|
|
8
|
+
status: active
|
|
9
|
+
updated: 2026-10-19
|
|
10
|
+
owners: [akhokhani]
|
|
11
|
+
files:
|
|
12
|
+
- talos-backend/libs/aegra-api/src/aegra_api/services/talos_chat_service.py
|
|
13
|
+
- talos-backend/libs/aegra-api/src/aegra_api/api/talos_chat.py
|
|
14
|
+
- talos-backend/libs/aegra-api/src/aegra_api/models/talos_chat.py
|
|
15
|
+
- talos-backend/libs/aegra-api/src/aegra_api/settings.py
|
|
16
|
+
- talos-backend/docs/talos-chat-security.md
|
|
17
|
+
related:
|
|
18
|
+
- ai-service-endpoints.md
|
|
19
|
+
- bedrock-models-and-cost-attribution.md
|
|
20
|
+
- ../../togatech/features/talos-chat-widget.md
|
|
21
|
+
- ../../bdr/features/landing-chat-drawer.md
|
|
22
|
+
- ../../bdr/features/campaign-chat-handoff.md
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
Abuse controls on the public website chat (`POST /api/ai/talos-session` → `POST /api/ai/talos-chat`): Google reCAPTCHA v3 on session mint, token replay guard, one lead email per session, configurable rate limits and the proxy-count gap; open when the togatech/BDR chat gets 403/503, a lead is emailed twice, or limits need tuning.
|
|
26
|
+
|
|
27
|
+
## Summary
|
|
28
|
+
|
|
29
|
+
The website chat has no user login. Protection is layered: the browser header gate (Origin/Referer/Sec-Fetch/UA, see [ai-service-endpoints](ai-service-endpoints.md)), Redis rate limits, a **reCAPTCHA v3 check when the session is minted**, then a 15-min HS256 session JWT bound to fingerprint + hashed IP + hashed UA. `/talos-chat` needs that JWT as `Bearer`, and the model, prompt and output schema are pinned on the server. The repo's own write-up is `docs/talos-chat-security.md` (commit `c3fffd27` on `_beta`; PR agilantsolutions/talos-backend#209 `_beta`→`_main`, open as of 2026-10-19).
|
|
30
|
+
|
|
31
|
+
## How it works
|
|
32
|
+
|
|
33
|
+
**Session mint order:** header checks → rate limit → reCAPTCHA → mint JWT.
|
|
34
|
+
|
|
35
|
+
**reCAPTCHA v3 (settings `TalosChatSettings` in `settings.py`):**
|
|
36
|
+
- The request body takes `recaptcha_token` (max 4096 chars). The client mints it with action `talos_session`.
|
|
37
|
+
- The server POSTs to Google `siteverify` with `TALOS_CHAT_RECAPTCHA_SECRET_KEY` + `remoteip`. It requires `success`, a matching `action`, a `hostname` in `TALOS_CHAT_RECAPTCHA_HOSTNAMES` (`togatech.com,*.togatech.com`), and `score >= TALOS_CHAT_RECAPTCHA_MIN_SCORE` (0.5).
|
|
38
|
+
- `TALOS_CHAT_RECAPTCHA_MODE`: `off` (code default) | `monitor` (verifies and logs, never blocks) | `enforce` (generic **403** on failure, **503** if Google is unreachable). `enforce` without a secret fails settings validation at startup.
|
|
39
|
+
- Logs: `talos_chat_recaptcha_result` (score, reason) and `talos_chat_recaptcha_rejected` with reason `missing_token`, `not_successful`, `action_mismatch`, `hostname_not_allowed`, `score_missing`, `low_score` or `replayed_token`.
|
|
40
|
+
- Env (2026-10-19): `.env.beta` = `enforce`, `.env.prod` = `monitor`. The secret lives only in the env files. Plan: flip prod to `enforce` once togatech PR #44 is deployed and real prod scores look right.
|
|
41
|
+
|
|
42
|
+
**Replay guard:** the SHA-256 of each accepted token is stored in Redis for 5 min (`talos:session:recaptcha:<digest>`). A second use is rejected as `replayed_token`.
|
|
43
|
+
|
|
44
|
+
**One lead per session:** the website emails a lead whenever a response carries every required output-schema field (`name`, `email`, `company`, `phone`, `intent`, derived from `TALOS_OUTPUT_SCHEMA` `required=True`). `/talos-chat` releases a complete lead **once per session `jti`** (Redis `talos:chat:lead:<jti>`). Later responses keep `message` / `suggestions` / `cta` and drop the lead fields. Internal service-key callers are unchanged.
|
|
45
|
+
|
|
46
|
+
**Rate limits are settings now:** `TALOS_CHAT_SESSION_MAX_PER_IP_PER_MIN` / `_PER_DAY`, `TALOS_CHAT_MAX_PER_IP_PER_MIN`, `TALOS_CHAT_MAX_PER_FINGERPRINT_PER_MIN` / `_PER_DAY`. Global budgets went back to 300/min and 10,000/day (they were 100,000 during the "TEMP firefight").
|
|
47
|
+
|
|
48
|
+
## Gotchas
|
|
49
|
+
|
|
50
|
+
- **Google `siteverify` returned `success:true` for the same v3 token twice** in live testing. Do not count on Google to reject replays. The Redis replay guard above is what stops them.
|
|
51
|
+
- **Scores depend on the browser.** An automated or extension-driven browser scored 0.3, and the same browser later scored 0.9. Always run `monitor` first and read real `talos_chat_recaptcha_result` scores before you switch to `enforce`.
|
|
52
|
+
- **Per-IP limits stay lifted in prod and beta** because `TALOS_CHAT_TRUSTED_PROXY_COUNT` is unset behind the EB nginx + ALB, so every visitor resolves to the same peer IP. Open follow-up: verify the hop count (likely 2: ALB + nginx), set it, then tighten the per-IP limits. Until then the fingerprint, global and reCAPTCHA checks carry the load.
|
|
53
|
+
- **Every consumer must send a token once its env is in `enforce`.** A session request with no `recaptcha_token` gets a 403 `missing_token`. Consumers that degrade silently (the BDR drawer) then look like they work but answer from a script. Note that the hostname allowlist only covers `togatech.com` domains.
|
|
54
|
+
- **The lead email itself is not protected by any of this.** The website sends it straight from the browser to TogaHub `email-templates/sendEmail`, which checks no reCAPTCHA (see [togatech talos-chat-widget](../../togatech/features/talos-chat-widget.md)). Talos also calls `sendEmail` server-to-server for reminder and scheduled-run emails (`services/notification_provider.py`, per-tenant TogaHub API key exchanged for a bearer), so any TogaHub lockdown must exempt authenticated service callers.
|
|
55
|
+
|
|
56
|
+
## Related
|
|
57
|
+
- [ai-service-endpoints](ai-service-endpoints.md) — endpoint map and the browser header gate
|
|
58
|
+
- [togatech talos-chat-widget](../../togatech/features/talos-chat-widget.md) — client side (token minting)
|
|
59
|
+
- [bdr landing-chat-drawer](../../bdr/features/landing-chat-drawer.md) · [bdr campaign-chat-handoff](../../bdr/features/campaign-chat-handoff.md) — the other `/talos-session` consumer
|
|
60
|
+
|
|
61
|
+
## Change history
|
|
62
|
+
- 2026-10-19 — created: reCAPTCHA v3 on session mint (off/monitor/enforce), Redis replay guard, one lead per session `jti`, configurable rate limits, proxy-count gap (akhokhani)
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
| [NavV2 Global Nav — menus, panel shell, scroll reveal, Talos FAB](features/navv2-global-nav.md) | togatech NavV2 header (designer "phase one"): data-driven menus, one shared panel shell + WebGL canvas, `data-nav-reveal` scroll hide/show, opt-in `data-nav-pin |
|
|
8
8
|
| [Platform Product Page (/our-platform) — ported designer prototype](features/platform-product-page.md) | togatech `/our-platform` TOGa Platform product page, ported from designer prototype `our-platform-v5`; open for the page-root-as-`<html>` rule, effect-order rul |
|
|
9
9
|
| [SEO / AEO / GEO — Prerendering, Single-Source Meta & Structured Data](features/seo-aeo-geo-prerender.md) | togatech SEO/AEO/GEO — build-time Puppeteer prerender + hydration, code-split routes (`lazyRoute`), single-source per-page meta + JSON-LD; open for the robots d |
|
|
10
|
+
| [Talos chat widget — session mint, reCAPTCHA v3 token, lead email path](features/talos-chat-widget.md) | togatech.com Talos chat client: how it mints a session (fingerprint + reCAPTCHA v3 token), how the lead email goes out through TogaHub `sendEmail`, and the open |
|
|
10
11
|
| [Talos Product Page (/talos) — ported designer prototype](features/talos-product-page.md) | togatech `/talos` Talos AI product page, ported from a designer HTML/JSX/CSS prototype; open for its talos.css port edits, section notes, open port work and pla |
|
|
11
12
|
| [Creating Pull Requests on togatech](workflows/creating-pull-requests.md) | Open a PR on `agilantsolutions/togatech` — `gh` CLI lacks `read:org`, so use a raw GitHub REST call with the keychain token; base is `_production`. |
|
|
12
13
|
| [Porting a Designer Prototype to a togatech Page](workflows/designer-prototype-port.md) | togatech procedure for turning a designer HTML/JSX/CSS prototype into a live page (`/talos`, `/our-platform`): where drops and port tools live, generated prefix |
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Talos chat widget — session mint, reCAPTCHA v3 token, lead email path
|
|
3
|
+
framework: "standalone"
|
|
4
|
+
repo: togatech
|
|
5
|
+
project: TOGA Technology Website
|
|
6
|
+
client: shared
|
|
7
|
+
type: feature
|
|
8
|
+
status: active
|
|
9
|
+
updated: 2026-10-19
|
|
10
|
+
owners: [akhokhani]
|
|
11
|
+
files:
|
|
12
|
+
- togatech/src/lib/talosSession.ts
|
|
13
|
+
- togatech/src/lib/talosRecaptcha.ts
|
|
14
|
+
- togatech/src/lib/api.ts
|
|
15
|
+
- togatech/src/components/Footer/Footer.tsx
|
|
16
|
+
- togatech/src/pages/Contact/view/components/ContactForm/ContactForm.tsx
|
|
17
|
+
related:
|
|
18
|
+
- ../architecture.md
|
|
19
|
+
- talos-product-page.md
|
|
20
|
+
- ../../talos-backend/features/website-chat-security.md
|
|
21
|
+
- ../../talos-backend/features/ai-service-endpoints.md
|
|
22
|
+
- ../../../../2.0/apps/_underscore/features/email-template-sending.md
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
togatech.com Talos chat client: how it mints a session (fingerprint + reCAPTCHA v3 token), how the lead email goes out through TogaHub `sendEmail`, and the open security gaps on that path; open when the site chat gets 403s, reCAPTCHA errors, or when touching forms or emails on the website.
|
|
26
|
+
|
|
27
|
+
## Summary
|
|
28
|
+
|
|
29
|
+
The site chat talks to Talos through a public two-step flow: `POST /api/ai/talos-session` mints a session JWT, then `POST /api/ai/talos-chat` carries the turns (server rules in [talos-backend website-chat-security](../../talos-backend/features/website-chat-security.md)). The session request now also carries a Google reCAPTCHA v3 token (PR agilantsolutions/togatech#44, base `_production`, open as of 2026-10-19). When Talos returns a complete lead, the **browser** emails it through TogaHub, just as the footer and contact forms do.
|
|
30
|
+
|
|
31
|
+
## How it works
|
|
32
|
+
|
|
33
|
+
1. `src/lib/talosRecaptcha.ts` reads `window.grecaptcha`, which `GoogleReCaptchaProvider` in AppLayout loads (it mounts only when `VITE_RECAPTCHA_V3_SITE_KEY` is set), and runs `execute` with action `talos_session`.
|
|
34
|
+
2. `src/lib/talosSession.ts` `mintSession` sends `{ fingerprint, recaptcha_token }`. If the script or site key is missing it sends no token and lets the backend decide (403 in `enforce`, pass in `monitor`/`off`). The change is backward compatible: a backend without the check ignores the extra field.
|
|
35
|
+
3. Chat turns use `Bearer <session token>`. The backend releases a complete lead once per session.
|
|
36
|
+
4. **Lead and form emails:** the footer form, the contact page form and the Talos chat lead all call TogaHub `GET https://api.togahub.com/v2/email-templates/sendEmail` from the browser through `useSendEmail` (`src/lib/api.ts`). It uses template `f64f8b8d-9238-4367-81d9-55c2bff277b2`, recipient `website@togatech.com`, and the contact page adds its own to/cc addresses. The uuid, to, cc and body values all travel in the URL.
|
|
37
|
+
|
|
38
|
+
## Gotchas
|
|
39
|
+
|
|
40
|
+
- **⚠ `sendEmail` is callable by anyone.** Its endpoint, template uuid and recipients all sit in the public JS bundle, and the server checks no captcha, so a bot can call it directly. The Talos session checks cannot protect it. `Footer.tsx` and `ContactForm.tsx` generate a `recaptchaToken`, but `useSendEmail` **drops it** because it only forwards `fieldMapping` fields. Open TogaHub/PHP follow-up: verify reCAPTCHA in `sendEmail` (actions `footer_form_submit`, `contact_form_submit`, `talos_lead`; hostname `togatech.com`; score ≥ 0.5), pin recipients per template and ignore public `to`/`cc`, rate-limit, ideally switch to POST, and exempt authenticated service callers (Talos sends reminder emails through it).
|
|
41
|
+
- **⚠ `VITE_HUBSPOT_SERVICE_KEY` (a HubSpot private-app token) is compiled into the public bundle.** Any `VITE_*` value ships to the browser. It should move server-side.
|
|
42
|
+
- **The page loads two reCAPTCHA release scripts.** Calling `grecaptcha.execute` from an isolated or extension context (for example a test harness that injects a script) throws `auto_render_clients` errors. Code that runs in the page context works, so test token minting through the app itself.
|
|
43
|
+
- **Low scores from automation are normal.** An automated or extension-driven browser scored 0.3 (0.9 later in the same browser). Do not take a single test run as the real-user score.
|
|
44
|
+
- **ESLint cannot run in this repo** (a legacy `.eslintrc` with ESLint 9). Use `tsc --noEmit` as the working check.
|
|
45
|
+
|
|
46
|
+
## Related
|
|
47
|
+
- [talos-backend website-chat-security](../../talos-backend/features/website-chat-security.md) — server side of the session/reCAPTCHA/lead rules
|
|
48
|
+
- [talos-backend ai-service-endpoints](../../talos-backend/features/ai-service-endpoints.md) — header gate and allowed origins
|
|
49
|
+
- [_underscore email-template-sending](../../../../2.0/apps/_underscore/features/email-template-sending.md) — the `sendEmail` server side
|
|
50
|
+
- [talos-product-page](talos-product-page.md) · [architecture](../architecture.md)
|
|
51
|
+
|
|
52
|
+
## Change history
|
|
53
|
+
- 2026-10-19 — created: session mint now sends a reCAPTCHA v3 token (PR #44); recorded the open `sendEmail` and HubSpot-key exposure (akhokhani)
|
package/package.json
CHANGED