PIF: COMP marker for ingredients whose MoS is assessed via their components
Some ingredients cannot be given a Margin of Safety of their own — their assessment lives somewhere else. The PIF already handled one such case: a plant extract whose safety follows from its solvent, marked "sol" in the ingredient catalogue. Essential oils need the same treatment for a different reason: their safety follows from their individual constituents, not from the whole oil. Until now they landed in the "no safety data" bucket and were flagged for review even though the assessment existed.
The new "comp" marker closes that gap. It behaves exactly like the solvent marker — no MoS computed, no concentration ceiling, verdict Biztonságos — and differs only in what it says. The PIF PDF now carries that explanation in two places: a line beside the ingredient's component table in §2.7, and a footnote on its row in the §2.8 MoS table. Both texts are seeded in Hungarian and English and can be reworded at /pif/templates without a code change.
Shipped in cosahh-rest (devel 9e22c81, master d4e3c8f) and rubedyne (devel f8da17a8, master 4751c1e3), covered by new tests, both suites green. No database change.
Awaiting deploy — backend first, then web. One ingredient carries the marker today, Boswellia Carterii Gum Oil in PIF 16; after deploy that dossier needs a Recalculate for its verdict to update.
cosahh-restpdf-exportpifsccsundeployed
FEATURE
Fold aggregated fragrance allergens into the PIF Margin of Safety calculation
Fragrance allergens declared on a supplier's sheet now receive a full SED/MoS safety assessment in the SCCS Calculations table and the TID PDF §2.8 table. Previously such allergens got no assessment at all, and allergens appearing in both the formula breakdown and a declaration sheet had their concentration understated — which overstated the Margin of Safety. Shipped to production: cosahh-rest 7b0c2ae, rubedyne 471c6d23. Each existing PIF needs one "Reload formula" to pick it up.
allergenspifregulatorysafety-assessment
Enhancements1
ENHANCEMENT
External API: allow labels to be changed on PATCH, not only set at create
The external API can now correct a ticket's labels, not only set them at creation. PATCH /api/v1/external/tickets/{displayId} accepts a `labels` array as a replace-set: omit the field to leave the labels untouched, send the full list to change them, send [] to clear them. Unknown names are created, exactly as at creation, and labels are applied before priority and status so one call can correct a label and close the ticket on the corrected set. punchline-mcp 1.3.0 carries the field through update_ticket. Label names are also trimmed and blank names refused, so a pasted " api" no longer becomes a second label indistinguishable from "api".
external-apimcppunchline
Bug fixes12
BUG
formulator-ai: az LLM_TIMEOUT beállítás soha nem hat — a kód LLM_TIMEOUT_SECONDS-t olvas
The LLM request deadline is now configurable and actually takes effect. It moved to `llm.timeout_seconds` in `config.yaml` (override with `LLM_TIMEOUT_SECONDS`), the default rose from 120 to 240 seconds, and the value in force is logged at startup. Long analyses — a large composition with an extensive allergen list and wide reference context — previously failed with a server error just past two minutes, regardless of what had been configured.
configformulator-aisilent-misconfiguration
BUG
External API cannot close its own tickets in a triage-enforcing project — no way to record a priority ruling
I keep the triagle function, but disabled in the Rubedyne project.
apiexternal-apitriageworkflow
BUG
PIF SCCS/MoS table counts an ingredient twice when its material restates it as a declared allergen
The SCCS Calculations table double-counted any ingredient whose material both lists it in its INCI breakdown and declares it on its allergen sheet. On PIF 16 "Borsmenta illóolaj" is peppermint oil, so Mentha Piperita Oil was reported at twice its true amount — halving the Margin of Safety and running the annex ceiling checks against a doubled concentration.
A declaration and a composition row are two statements about the same material, not two masses. The widening step now adds only the part a declaration contributes over and above what the declaring material's own composition already accounts for. The overlap is measured per declaring material, so an allergen dosed neat *and* declared inside a parfum still counts as the two real masses it is.
Fixed in cosahh-rest (devel aa99932, master e91e140), covered by five new tests, full suite green. Because the correction is applied when the document is read, already-archived dossiers were fixed without re-freezing them.
DEPLOYED and verified on 2026-08-25. PIF 16 was recalculated: Mentha Piperita Oil now reports 0.30 % — its material's actual content, counted once — with SED 0.391 and MoS 102.30, confirmed directly against the database. The buggy code would have produced 0.60 % and SED 0.782.
allergenscosahh-restpifsccsundeployed
BUG
Production MySQL is listening on all interfaces and reachable from the internet
MySQL is no longer reachable from the internet. Ports 3306 and 33060 are closed at the firewall, with the docker bridge networks and loopback explicitly allowed so every application keeps its database connection — no application config changed and nothing needed redeploying. Verified from off-host: both ports blocked, SSH and HTTPS unaffected, all 17 containers healthy with zero DB errors, and a DB-backed endpoint still returning 200. Rules persisted with netfilter-persistent.
The fix originally proposed on this ticket (bind-address = 127.0.0.1) was deliberately not used: containers reach MySQL over the host's public IP, so it would have severed every application on the box.
Direct Workbench connections to proding.co:3306 no longer work. Use Standard TCP/IP over SSH — SSH host proding.co:22 with the existing key, MySQL host 127.0.0.1 port 3306. Confirmed working end to end.
databaseinfrastructuresecurity
BUG
Reward sweep: a row whose billing resolution keeps throwing is retried forever and never quarantined
Fixed and deployed. Billing-owner resolution now happens inside the sweep's per-row loop, after the attempt has been recorded, instead of during candidate mapping upstream of it. A row whose resolution keeps throwing is therefore counted, backed off and quarantined at the attempt cap like any other poison row, rather than being skipped before its counter could rise and retried hourly forever. Fixed by removing the special case rather than adding a guard: the Candidate now carries the row's own user id, so a resolution failure is simply an ordinary per-row failure.
cosahh-restreward-sweep
BUG
Reporters can set their own reward tier: external-API tickets are auto-triaged from reporter-supplied priority
Fixed at the source and deployed. A SYSTEM service account filing a ticket no longer counts as a triage ruling, so a ticket filed through the external API or email intake arrives with triagedPriority null regardless of the priority sent. Only an AGENT or ADMIN ruling can set the field the token reward is tiered off, closing the self-service path for every caller rather than one. Verified in the deployed artifact; production exposure at fix time was nil. The cosahh-rest caller-side clamp remains as defence in depth and is tracked for removal in RUBE-40.
cosahh-restpunchline-apirewardssecuritytokens
BUG
Reward credits stranded forever: the reconciliation sweep for unapplied RewardGrants does not exist
Delivered and live in production. A scheduled sweep now settles reward rows whose token credit never landed, and it cannot double-pay: the durable half is guarded by a conditional applied_at stamp and the Redis half by a per-row credit-once marker. Schema applied, sweep enabled with an activation cutoff of 2026-08-19T18:23:24Z. Follow-ups tracked in RUBE-34 and RUBE-35.
cosahh-restdata-integrityrewardstokens
BUG
Lab Report templates and style config are global tables reachable by customer-side admins
⚠️ **Closed as documented, NOT as remediated. The exposure is still live in the code.**
Verified at close: `SecurityConfig.java:304-307` still reads
```
.requestMatchers("/lab-reports/templates/**").hasAnyAuthority("admin:formula", "admin:pif")
.requestMatchers("/lab-reports/style-config").hasAnyAuthority("admin:formula", "admin:pif")
```
and `LabReportTextTemplate` still has no brand column. So a customer-side `pif-admin` or `formula-admin` can rewrite and restyle the Lab Report / CoA PDFs of **every other tenant**. Nothing about that has changed since this was filed.
**Why it was filed.** Found by the final review of the PIF config lockdown (RUBE-27's sibling work). That branch closed the identical hole for `PIFTextTemplate` and `PIFStyleConfig` — global, non-brand-scoped tables reachable with ordinary PIF permissions — and the same reasoning applies verbatim here, three lines away in the same file. It was left out of that branch deliberately, to avoid expanding a reviewed change.
**The fix, if picked up later**, mirrors what already shipped for PIF: seed a dedicated authority in `SeedAuthzCatalog.sql` mapped to the `admin` role only, add `SecurityConfig` matchers above any broader rule (first match wins), add class-level `@PreAuthorize` on the controllers, switch the managing frontend page to `isGlobalAdmin`, and extend `scripts/pif-config-authz-smoke.sh` with lab-report rows including a regression row proving ordinary lab-report access still works.
⚠️ The authority must be seeded in the **database**, not Auth0. `cosahh-rest` resolves authorities via `DbAuthoritiesConverter` from `user_role → role → role_permission → permission`; the Auth0-claim fallback defaults off and applies only to users with zero DB roles. An Auth0-only grant grants nothing — that exact mistake was made in the PIF spec and caught only by the final review.
Not known to have been exploited; this is a permissions-model gap, not an incident. Re-open or re-file if the exposure is to be closed.
labreportmulti-tenancyrubedynesecurity
BUG
Bug reward tier keyed off a reporter-settable priority
The bug-reward payout was tiered by a ticket's priority — a field reporters can set themselves when filing. Since closing a ticket never required re-checking that priority, a reporter-filed "CRITICAL" could be closed as-is and pay the top reward on a number the reporter chose.
The reward now reads a separate priority that only staff can set, recorded when someone with edit rights rules on the ticket. Until that happens the ticket is untriaged and pays nothing, so the reward is something a team member grants rather than something a reporter proposes.
Caught by an adversarial review before the feature shipped, so no tokens were ever paid out this way. Fixed in both the sender and the receiver, with tests covering the case where staff confirm a reporter's priority unchanged — which previously would have left the ticket unrewarded.
punchlinerewardssecuritytokenswebhook
BUG
PIF renders and archives from the live formula instead of the frozen snapshot
Implemented and merged to `devel` in both repos (cosahh-rest `bef37f8`, skinlyzer `99c9ff2c`, both pushed). **NOT DEPLOYED** — see outstanding items below before treating this as shipped.
A formula edit leaked into a newly archived PIF version PDF. The freeze machinery existed but was wired only into the display path; every render path resolved live, and the archive simply delegated to those. Fixing the service layer alone was not enough: `PIFTidPdfService` independently re-read the live graph for §2.1, §6.1, §7 and the allergen tables, so the very section the snapshot exists to reproduce would still have rendered live.
Delivered: a `CompositionRow` seam with two sources (live formula / frozen rows) and two projections; new `PIFFormulaMaterial` and `PIFFormulaAllergenic` tables; `generate()` taking the frozen composition, per-occurrence raw materials, allergen totals and threshold as parameters, with `ForMatRepository` no longer injectable into the renderer so the bug cannot silently return; `ensureFrozen` in its own transaction across nine entry points; and the export gate moved to freeze time. Frontend warns in the New Version dialog when the formula has drifted. Backend 1068 tests green, frontend 286 files.
**Outstanding before this is live:**
1. Apply `AddPIFFormulaMaterial.sql` — but reconcile against production first. `mvn test` was booting a `@SpringBootTest` against the production datasource with `ddl-auto=update`, so Hibernate very likely created these objects already. The migration is idempotent and carries `MODIFY COLUMN`, but the live schema must be diffed against it.
2. Deploy cosahh-rest and skinlyzer together.
3. **The manual end-to-end smoke has never been run** — it needs a local MySQL, and this machine's default datasource is production. It remains the only end-to-end proof the fix works.
Release-note items (every existing PIF's freeze point resets on deploy day; a pre-drifted PIF freezes from the edited formula; MaterialAllergenic corrections no longer reach a PIF until Reload; allergen totals may shift) are documented in `skinlyzer/docs/superpowers/plans/2026-08-08-pif-frozen-formula-DEPLOY-NOTES.md`.
Related: RUBE-24 (the fingerprint work that exposed this).
complianceformulapifrubedyne
BUG
PIF formula change detection must track material changes, not the derived INCI list
Delivered and on `master` in both repos.
PIF drift detection compared the saved snapshot at INCI level, so swapping raw material A for material B with an identical INCI profile at the same effective concentration left the badge green and the Reload button disabled — the PIF silently kept referencing a material no longer in the formula.
Now a SHA-256 fingerprint of the formula's per-material composition is persisted on `PIFDocument` and recomputed from the live formula on read. `GET /pif/{id}/formula-sync` serves it; the frontend consumes it through a single SWR hook. Material swaps, concentration changes, MatIng edits and phase changes are all detected; row re-ordering is not a false positive.
Detection is not retroactive — it begins from the first read after deploy, since no material-level history existed to compare against. Documents predating the fingerprint fall back to the old INCI-level comparison until they are frozen, which is why the legacy `PIFFormula` table is deliberately retained.
Closing this one scoped to the fingerprint work. The render-path defect it exposed — that the frozen snapshot was never consulted when generating the PDF — is tracked separately as RUBE-27, now linked.
complianceformulapifrubedyne
BUG
PIF document images render as "[Image not available]" in the PIF & CPSR PDF
Fixed in cosahh-rest `devel` commit `e465472`.
PIF documents are uploaded to Cloudinary with `access_mode=authenticated`, so their stored URL answers 401 to an unsigned server-side fetch. The PDF renderer fetched that URL directly, so every embedded PIF image failed and fell back to the "[Image not available]" placeholder. Downloads always worked because the UI goes through the frontend's signing proxy — the renderer was never taught to do the same.
The renderer now signs the URL first, via a new `CloudinaryService.signedRawUrl` that mirrors the frontend signer. Backend suite green at 1073 tests.
Not yet deployed. Two related gaps were found and deliberately left for separate tickets: the Annex document links in the PDF are the same unsigned URLs and will 401 when clicked, and the image filter still admits `webp`, which OpenPDF cannot decode.
cloudinarypdfpifrubedyne
Under the hood2
TECHNICAL
cosahh-rest port 8000 is exposed to the internet in cleartext alongside the TLS proxy
Fixed and deployed. The container's port 8000 is now bound to 127.0.0.1 instead of 0.0.0.0, so the app is no longer reachable from the internet in cleartext. Apache is unaffected — it terminates TLS and proxies to http://127.0.0.1:8000/ for both :80 and :443. Verified from off-host after deploy: https://proding.co/ serves normally, the webhook receiver still answers through the proxy, and http://proding.co:8000/ is refused. Dependents were enumerated before the change: no container env referenced the published port, nothing in-repo hardcoded it, the healthcheck runs inside the container, and the webhook is registered against the HTTPS URL.
cosahh-restinfrastructuresecurity
TECHNICAL
The only test that executes the credit-once Lua script runs in no automated build
Fixed and deployed. An "it" Maven profile now runs RedisTokenCounterStoreIT — the only test anywhere that executes the credit-once Lua script — via `mvn verify -Pit`. The profile deliberately skips surefire, because a plain `mvn verify` would run the unit suite and this project's datasource defaults to the production database, so an unscoped run boots a Spring context against prod. The command is recorded in CLAUDE.md and in the activation runbook's pre-flight checks rather than only in a commit message, so it is findable by whoever needs it before a deploy that touches the counter store or the reward policy.