mirror of
https://github.com/Sea-Haven-Industries/procurement-ingest.git
synced 2026-09-30 22:23:14 +00:00
|
Some checks are pending
Deploy / deploy (push) Waiting to run
* feat: PO ai-fallback fail-closed gate + prompt hardening, parity with #104 (refactor phase 1) Ports WO's #104 AI-fallback security hardening to the PO email processor, adapted for PO's nested contract instead of copying the WO gate verbatim. validate_ai_fallback() (template_parser.py) fail-closes raw Bedrock output before it reaches enrich_parsed or any dispatch/save: recursive key-set check with missing-key normalization (nested contract: supplier{}, ship_to{}, line_items[]); po_number checked against the same hardened prefix+hyphen+digits regex family that guards the DynamoDB partition key the handler builds from it (rejects fullwidth-digit and trailing-artifact injection); email_type enforced against the {new_po, revision, cancellation} allow-list before dispatch so a miss can never fall into the else -> save_new_po branch; money fields accept Decimal/int/None only, matching PO's parse_float=Decimal decode (a float-typed check would be wrong here). A gate failure emits ParseMethod=ai_fallback_rejected and `continue`s to the next record -- it never raises, so attacker-controlled input can't churn the retry/DLQ path. extract_with_claude() wraps the untrusted email in an <email> data block and neutralizes forged <email>-tag lookalikes in the body with the same linear-time regex approach as WO's _EMAIL_TAG_RE, and sets temperature=0 on the Bedrock call. Deliberate double-count: PO emits ParseMethod=ai_fallback before the Bedrock call (so a Bedrock-side error still records the outcome), so a rejected email always produces both an ai_fallback datapoint (pre-call) and an ai_fallback_rejected datapoint (post-gate). This is intentional, not a bug -- documented in handler.py, template_parser.py, and the README. cdk/po_stack.py: in-place property update to the existing po-email-processor-template-fallback-rate alarm (same logical ID, no rename/replacement) -- the fb/(fb+tmpl) expression is left byte-identical to its pre-Phase-1 form and ai_fallback_rejected is deliberately excluded from the numerator/denominator/volume floor, since folding it in as WO does would double-count every rejection (PO's pre-call emit already counts it once via fb). A net-new EmailProcessorAiFallbackRejectedAlarm watches the rejected series on its own, retuned for ~57 emails/day with the 6h/IF-floor/eval-4/ datapoints-2 idiom (not WO's 5-minute sparse idiom, which is structurally dead at PO volume). Both alarms remain ALARM-only to site-alerts, NOT_BREACHING, with no element-wise MAX in the math (post-#102 rule). * Block "Cancelled" po_status off the AI cancellation route The AI-fallback gate type-checked po_status but let any string through, unlike the template path which never emits "Cancelled" on a new_po. Dispatch routes on email_type, so an AI-path new_po or revision carrying po_status="Cancelled" would reach save_new_po/save_revision and cancel a live PO via _merge_update's sticky-cancel write without ever hitting save_cancellation. Reject the exact sticky marker on any non-cancellation email_type so the AI path matches the template path's guard; arbitrary non-marker status strings still pass. email_type is already validated to the enum before this check, and a cancellation reaches save_cancellation (which hardcodes the status), so po_status stays irrelevant on that route. |
||
|---|---|---|
| .. | ||
| po | ||
| wo | ||