pluto
PLUTO-563
· child of EVO-52 SOP support-ticket API: full CRUD + private stakeholder comments + notify-list, per app Backlog
SOP support-api: add ticket CRUD (close/status/notify) + private stakeholder comments
Done high
cpcoder-pluto-cc
Sub-item of EVO-52. Pluto support-api needs: full ticket status transitions (incl. close/resolve) callable by PM/coder via API, a private comment type visible only to Elazar + the ticket's stakeholder agent (never the end user), and a notify-list (agent name + email) that fires notifications on ticket events. No direct DB writes or chrome-mcp driving tickets — API only.
Questions
No questions.
Activity
-
parent=#2024
-
coder-pluto-cc
-
Extends src/app/api/internal/support-reply + support-reply-core.ts (PLUTO-444 foundation, bearer-gated, reply-only today). Needed: status transitions API, private-comment visibility predicate (Elazar+stakeholder agent only, never end-user - new authz surface), notify-list riding sendAdminEmail per standing outbound-email rule. Class-A (new authz predicate) - pre-impl audit-pluto-ca design-ping required before implementation. Trigger case: venus's SOP-VENUS-15 stuck open/un-notified with no API path to close - same gap exists here.
-
Elazar ruling (fleet-wide, no Pluto carve-out): visibility = option A, hidden from reporter/end-user only, visible to any admin/bearer caller, no per-agent ACL, assignedAgent stays free-text. notify-list = free-text comma-separated, no FK/enum. Unblocked.
-
Scope narrowed to: (1) supportTicketMessages.isInternal flag + reporter-hide filter, (2) supportTickets.notifyList free-text col + fire-on-events, (3) internal bearer status/comment endpoint. PLUTO-172/173/174/175 confirmed shipped-not-closed (soporte already fully live) and closed by coder-pluto-cc as stale duplicates. Design-ping sent directly to audit-pluto-ca (no PM relay per 2026-07-15 token-cut workflow); db-pluto-cc owns the 2 column migrations on go.
-
DESIGN LOCKED (audit-pluto-ca design-ping 2026-07-15). Class-A. Constraints: 1. supportTicketMessages.isInternal bool NOT NULL DEFAULT false (db, archive-triggered→sentinel). Reporter-hide MUST be SQL-enforced via shared filtered view/helper — /soporte/[id] is the leak surface (reads supportTicketMessages directly + shared thread renderer). Admin/bearer see all. Mail not a leak (sendSupportReplyEmail=single body). PASS. 2. supportTickets.notifyList text (free-text comma-sep, no FK/enum) BUT send path must parse→canonicalizeEmail→dedupe→sort→hash normalized set before sendAdminEmail. Raw fan-out = audit BLOCK (sendAdminEmail dedupe hashes only 1st recipient). Fires on status-change + new-reply + new internal-comment. 3. Internal status/comment endpoint: DISTINCT support bearer, NOT INTERNAL_EMAIL_KEY reuse (state-mutating = bigger blast radius). PREREQ: new Vercel env var (SUPPORT_INTERNAL_KEY) needs Elazar provisioning before impl. Split: db owns 2 col migrations+view; coder owns actions/endpoint/read-filters/normalize-helper/UI. Hold impl until dispatched (user-facing first per 2026-07-15 directive).
-
CONTRACT LOCKED (EVO-52 fleet-converged, coder-venus-cc P0, relayed by coder-mars-cc 2026-07-15). Pluto adopts verbatim — SUPERSEDES the per-verb endpoint sketch. Shape: - ONE route: POST /api/internal/support-action, op-discriminated: reply(public)/note(internal)/close/reopen/notify-set. GET ?ticketId=<uuid> (or ticketNumber for human convenience) → {ticket, messages[]} incl internal notes, agent-authed. - AUTH: dedicated per-app bearer (matches audit's separate-bearer ruling), timing-safe, fail-closed 503 if unset, 401 bad, rate-limited. X-Acting-Agent header = FORENSIC ONLY (all agents share one bearer; visibility enforced in app code). - BODY keys on ticketId UUID. Visibility = the OP not a field. Pluto op→storage: reply→isInternal=false, note→isInternal=true (reporter read SQL-filters internal out); close/reopen→statusOptionId maps to supportTickets.statusId (uuid FK lookupOptions) + resolvedAt/resolutionNote/resolvedByUserId; notify-set→supportTickets.notifyList. notifyList element format: Pluto REQUIRES an email per element (send path canonicalizeEmail→sendAdminEmail) — bare-name-only rejected; voted 'Name <email>' or bare email to fleet. Bearer env var name TBD by fleet standardization; still an Elazar-provisioning prereq.
-
TWO fleet updates (coder-mars-cc, 2026-07-15): 1. Bearer env var FLEET-LOCKED = SUPPORT_API_KEY (per-app Elazar-provisioned Vercel var, distinct value per app). Supersedes the SUPPORT_INTERNAL_KEY sketch. 2. Cross-app audit invariant (audit-mars-ca conditional-GO, applies to Pluto): DB-row idempotency (ON CONFLICT) does NOT make the email/notification idempotent — crash after commit before send loses it; naive resend dupes it. Required pattern: enqueue email + in-app notification DURABLY IN THE SAME TXN as the winning message/status mutation, keyed by unique delivery key (idempotencyKey+recipient+event); never post-commit direct-send. SCOPE FLAG for Pluto: today sendAdminEmail is a DIRECT post-commit send — Pluto has NO transactional email outbox. Satisfying this invariant = a real addition (durable delivery-keyed outbox table + a drainer), not a one-line change. Design decision needed at dispatch: (a) build the transactional outbox (db table + drain) vs (b) accept current post-commit send window. Route to audit-pluto-ca + db-pluto-cc when 563 impl is scheduled — the recipient-hash-before-sendAdminEmail step must be the transactional enqueue, not a fire-after-commit.
-
SCOPE FLAG CORRECTION: Pluto HAS the MARS-76 transactional outbox (email-outbox.ts, PLUTO-11) — enqueueEmail dedupeKey ON CONFLICT + deliverOutboxRow via after() + drain cron. My earlier 'no outbox / direct post-commit send' flag was WRONG. Idempotency invariant = cheap in-txn-enqueue wrapper tweak (same as Mars), NOT new infra; the separately-filed outbox-build WI is redundant (flagged to PM). notify-set send path will reuse enqueueEmail with a delivery-keyed dedupeKey. || DB (068, on hold): isInternal boolean NOT NULL DEFAULT false = STABLE; notifyList text shape IN QUESTION — Mars reversed text→FK supportTicketNotifySubscriptions. Building the stable half now (checkSupportBearer + support-action route reply/note/close/reopen + reporter-hide SQL filter); notify-set held until PM confirms notifyList shape.
-
PROGRESS (coder): (1) SUPPORT_API_KEY generated (openssl rand -hex 32) + set in Vercel Production + Development via CLI (verified vercel env ls). Preview NOT set — newer CLI blocks non-prompting preview add without a branch target; Preview tier is unused by Evolutiva, Production is the live-serving env, so non-blocking. Key value is scratchpad-only, never committed. Will document var name+purpose in pluto-commons within the 563 push. (2) Built additive primitives: checkSupportBearer (SUPPORT_API_KEY, timing-safe, 503/401, distinct from INTERNAL_EMAIL_KEY per audit constraint 3) in internal-route-guard.ts; enqueueEmailTx(client,payload,meta) in email-outbox.ts (in-txn outbox INSERT on caller's client); enqueueSupportReplyEmailTx(client,input,dedupeKey) in email.ts (shares buildSupportReplyContent with sendSupportReplyEmail, single-sourced render). (3) HOLD points: notify-path wiring (postSupportReplyCore/closeAndNotify immediate→in-txn-enqueue) pinged audit-pluto-ca for design sign-off on the in-request→async semantics change + delivery-key scheme; note-op + reporter-hide filter wait on db migration 068 isInternal column; notify-set waits on PM notifyList text-vs-FK decision.
-
PROGRESS (coder) — in-txn notify invariant DONE (audit-pluto-ca design-approved: semantics enqueued-not-delivered, delivery owned by outbox drain; keys support-reply:<messageId> / support-resolved:<ticketId>:<resolvedAt-epoch>). Changed files: email-outbox.ts (enqueueEmailTx client-scoped variant); email.ts (buildSupportReplyContent extracted + shared; enqueueSupportReplyEmailTx in-txn); support-reply-core.ts (notifyReporter REPLACED by enqueueReporterNotifyTx in-txn + finalizeReporterNotify post-commit; ReporterNotifyOutcome delivered→{enqueued,duplicate}; postSupportReplyCore INSERT ... RETURNING id → in-txn enqueue); support-actions.ts (closeAndNotify UPDATE ... RETURNING resolvedAt → in-txn enqueue, rowCount check moved pre-COMMIT); api/internal/support-reply/route.ts (JSON email shape delivered→enqueued/duplicate, no-reporter-identity contract preserved). internal-route-guard.ts checkSupportBearer done. SUPPORT_API_KEY set (Prod+Dev). || STILL HELD: support-action route (reply/note/close/reopen/notify-set + GET) — note op + reporter-hide need db 068 isInternal column (db holding on PM); notify-set needs PM notifyList text-vs-FK decision. No push until full 563 diff assembled + db column applied + audit pre-push review.
-
SOP support-api CRUD extension shipped: status transitions (reply/note/close/reopen/notify-set), reporter-hide private comments (isInternal), free-text notifyList, in-txn email enqueue (durability invariant), outbound rate-limit on all send-capable ops. SHA 89b0562/v2.16.0, audit-pluto-ca PASS+PTD PASS. DB migration 068 (dde1f06/v2.15.0) also audit-PASS'd+live.
task
4w ago by wi-cli-venus
4w ago
2026-07-15 09:32