Skip to content

feat(APP-266): support intake pipeline, Linear integration and guardrails for the assistant service (p2)#1224

Draft
tyhonchik wants to merge 1 commit into
mainfrom
app-266-customer-support-dialog-or-chatbot-p2
Draft

feat(APP-266): support intake pipeline, Linear integration and guardrails for the assistant service (p2)#1224
tyhonchik wants to merge 1 commit into
mainfrom
app-266-customer-support-dialog-or-chatbot-p2

Conversation

@tyhonchik

@tyhonchik tyhonchik commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

This adds the Phase-1 support-intake feature to the assistant service scaffolded in #1222. Previously the service exposed only /health — intake, Linear and guardrails existed only in the plan. Now /chat streams a deterministic intake conversation (classify intent → extract fields → respond), /issues creates the Linear ticket idempotently and /files/* handles attachments over Vercel Blob — the service becomes a working intake API; the chat widget (#1231) and the app wire-up follow in the next PRs of the stack.

The infrastructure layers are deliberately thin: session storage talks to @upstash/redis directly, both rate limits run on @upstash/ratelimit, CORS is hono/cors with an origin callback and model fallback is native AI Gateway — no hand-rolled KV/rate-limiter/CORS/fallback abstractions.

Changes

  • Vercel entrypoint fix. The hono preset resolves src/app.ts as the serverless entry and requires the Hono app as the default export — this is what broke assistant-livid.vercel.app/health. app.ts now default-exports the constructed app (real dependencies stay lazy) and src/index.ts is deleted so there is a single entry candidate.
  • Contracts (@aragon/assistant-contracts): chat/issue/file/error schemas, the data-collectedFields data part driving the live summary, shared assistantLimits and the pinned Phase-2 docSearchResultSchema. createIssueRequestSchema = chatRequestSchema — the server is stateless and re-extracts from the submitted transcript. Email is optional: asked for softly, never blocks creation.
  • /chat pipeline on AI SDK v7 via the AI Gateway (primary anthropic/claude-haiku-4.5): generateText + Output.object classify + extract on every turn, live-summary data part, streamText reply capped at 500 output tokens with an agentic respond prompt (reacts to what the user said, no form-like questioning). Off-topic, turn-limit (12) and token-budget (3000) short-circuit to fixed messages with zero model calls. Model fallback (google/gemini-2.5-flash) is native gateway routing via providerOptions.gateway.models; step logs record the actually served model.
  • Files over Vercel Blob — serverless request bodies cap at 4.5 MB, so bytes never travel through the function: POST /files/token issues a client token (@vercel/blob/client handleUpload, validating the client-proposed assistant/{sessionId}/{fileId}/{filename} pathname), the browser uploads directly to Blob, POST /files/confirm pins the URL to our store and prefix, downloads the bytes and runs magic-byte validation right there — invalid files are rejected at upload time, not at ticket time. DELETE /files/:fileId is idempotent with an LREM-guarded slot release (exactly once under races) and best-effort blob delete. Linear sees files only at POST /issues (re-validated, upload failure → 502 + claim release so retry works); after success the blobs are deleted. A daily cron GET /internal/cleanup (Bearer CRON_SECRET) sweeps orphaned blobs older than 24h.
  • File validation: magic-byte allowlist via file-type (png/jpg/gif/webp/pdf), text formats by extension + strict UTF-8 decode, SVG banned, display names sanitized, Content-Type always from sniffing. Asset URLs stay server-side in KV — clients can never inject foreign URLs into tickets.
  • /issues idempotency: atomic SET NX claim before the Linear call, released on failure so a retry with the same session succeeds; concurrent submits produce exactly one ticket (covered by a race test). User-authored content only ever appears in > blockquotes; the transcript is a collapsed section.
  • Rate limits on @upstash/ratelimit: per-IP sliding window (10 rpm) middleware on /chat, /issues, /files + fixed window (20 new sessions/day) consumed on a session's first turn — with a rollback when the limit refuses, so resending a refused session id cannot bypass the daily budget. All policy lives in lib/rateLimit.ts.
  • Session storage on direct @upstash/redis (Redis.fromEnv({automaticDeserialization: false}) — keeps LREM byte-exact and avoids double-parse); sessionStore.ts stays the single session API including the atomic file-slot reservation. CORS is hono/cors with a ~15-line origin resolver (lib/cors.ts): exact origins + *-prefix suffix patterns, preview scope narrowed to *-aragon-app.vercel.app.
  • Observability: structured stdout JSON per pipeline step — the log entry type has no free-text fields, so message content and emails structurally cannot leak; Sentry init in src/instrument.ts (before the app), source-map upload wired into shared-deploy.yml, unified JSON error shape via app.onError.
  • DI seam: createApp(overrides) with lazy getters (redis, sessionStore, linear, chatModel, blobStore) — tests never touch Upstash/Linear/Gateway, and /health keeps working with no secrets configured.
  • Tests cut to the critical set (46 assistant + 5 contracts): file validation security (forged extensions, SVG ban, binary-as-text), issue-body prompt injection, /issues idempotency + double-POST race + claim release, file-slot and claim races on a Redis fake (which emulates the two @upstash/ratelimit Lua scripts and fails loudly on script drift), chat guardrails (turn/token limits call no model), CORS/429/error-shape route tests, cron auth, and the cross-package pinned contract shape. Criterion: a failing test = a real incident.
  • Runtime env for deployments: shared-deploy.yml gains runtime-env/runtime-env-keys inputs feeding vercel deploy -e (the .env file only exists at build time); assistant deploys lift the 1Password secrets (+ BLOB_READ_WRITE_TOKEN, CRON_SECRET) to runtime, dev pins ASSISTANT_ENV=development.

Note

  • The deploy needs the runbook secrets in kv_assistant_*, including LINEAR_TEAM_ID (test team in development/preview, real support team in production), BLOB_READ_WRITE_TOKEN and CRON_SECRET.
  • Linear's +++ Transcript collapsible syntax is unverified against a live workspace — if it renders literally, the fallback is cosmetic only; check the first real ticket.
  • Model ids live in config.ts (anthropic/claude-haiku-4.5 primary, google/gemini-2.5-flash fallback) — final pick still open, one-line change.

🤖 Generated with Claude Code

@tyhonchik tyhonchik requested a review from a team as a code owner July 13, 2026 10:21
@linear-code

linear-code Bot commented Jul 13, 2026

Copy link
Copy Markdown

APP-266

@vercel

vercel Bot commented Jul 13, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
assistant Error Error Jul 13, 2026 10:45am

Request Review

@tyhonchik tyhonchik force-pushed the app-266-customer-support-dialog-or-chatbot-p2 branch from 44bc09f to 44ec6c7 Compare July 13, 2026 10:30
@tyhonchik tyhonchik force-pushed the app-266-customer-support-dialog-or-chatbot-p2 branch from 44ec6c7 to f25f0a3 Compare July 13, 2026 10:35
@tyhonchik tyhonchik force-pushed the app-266-customer-support-dialog-or-chatbot-p2 branch from f25f0a3 to aabe7b8 Compare July 13, 2026 10:44
@tyhonchik tyhonchik force-pushed the app-266-customer-support-dialog-or-chatbot-p2 branch from aabe7b8 to b4dca02 Compare July 13, 2026 14:09
@tyhonchik tyhonchik force-pushed the app-266-customer-support-dialog-or-chatbot-p2 branch from b4dca02 to 843b86c Compare July 13, 2026 14:20
@tyhonchik tyhonchik force-pushed the app-266-customer-support-dialog-or-chatbot-p2 branch from 843b86c to 1c99c34 Compare July 13, 2026 17:30
@tyhonchik tyhonchik force-pushed the app-266-customer-support-dialog-or-chatbot-p2 branch from 1c99c34 to 92802b8 Compare July 13, 2026 17:33
@tyhonchik tyhonchik force-pushed the app-266-customer-support-dialog-or-chatbot-p2 branch from 92802b8 to 0a4d477 Compare July 13, 2026 17:34
@tyhonchik tyhonchik force-pushed the app-266-customer-support-dialog-or-chatbot-p2 branch from 0a4d477 to 806563a Compare July 13, 2026 18:39
@tyhonchik tyhonchik force-pushed the app-266-customer-support-dialog-or-chatbot-p2 branch from 806563a to 79bca78 Compare July 13, 2026 20:38
@tyhonchik tyhonchik force-pushed the app-266-customer-support-dialog-or-chatbot-p2 branch from 79bca78 to d38e548 Compare July 13, 2026 20:51
@tyhonchik tyhonchik force-pushed the app-266-customer-support-dialog-or-chatbot-p2 branch from d38e548 to 9f480a4 Compare July 13, 2026 21:08
@github-actions

github-actions Bot commented Jul 14, 2026

Copy link
Copy Markdown

🚀 Preview Deployment: View Here
🤖 Assistant Preview: View Here (app preview points at it)

@github-actions

github-actions Bot commented Jul 14, 2026

Copy link
Copy Markdown

E2E results (preview)

Smoke

Base URL https://app-next-dtqlzn6mx-aragon-app.vercel.app
Suite smoke
Playwright result ⚠️ completed with flaky tests
Summary 78 passed, 1 flaky, 1 skipped
HTML report View report
GitHub job ✅ completed

View run #4529

@tyhonchik tyhonchik force-pushed the app-266-customer-support-dialog-or-chatbot-p2 branch from dbd0ed5 to 4cbed4c Compare July 14, 2026 13:32
@tyhonchik tyhonchik force-pushed the app-266-customer-support-dialog-or-chatbot-p2 branch from 4cbed4c to 9fe9cfa Compare July 14, 2026 14:03
@tyhonchik tyhonchik force-pushed the app-266-customer-support-dialog-or-chatbot-p2 branch from 9fe9cfa to 4a639ab Compare July 14, 2026 14:08
@tyhonchik tyhonchik force-pushed the app-266-customer-support-dialog-or-chatbot-p2 branch from 4a639ab to 67991fc Compare July 14, 2026 14:22
@tyhonchik tyhonchik force-pushed the app-266-customer-support-dialog-or-chatbot-p2 branch from 67991fc to 31f8ae0 Compare July 14, 2026 14:52
@tyhonchik tyhonchik force-pushed the app-266-customer-support-dialog-or-chatbot-p2 branch from 31f8ae0 to 64dd4d1 Compare July 14, 2026 15:02
@tyhonchik tyhonchik force-pushed the app-266-customer-support-dialog-or-chatbot-p2 branch from 64dd4d1 to 3d520bf Compare July 14, 2026 15:34
…ardrails to the assistant service

Contracts (@aragon/assistant-contracts): chat/issue/file/error schemas, the
data-collectedFields live-summary part, shared hard limits and the pinned
Phase-2 docs-search shape.

Assistant service: stateless /chat pipeline on AI SDK v7 via the AI Gateway
(classify intent -> extract fields -> ask for missing, full re-extraction every
turn), idempotent /issues creation in Linear (atomic claim before the call,
released on failure), /files uploads with server-side magic-byte validation
(SVG banned, names sanitized, asset URLs kept server-side), per-IP rate limits
via Upstash, structured PII-free step logs with Sentry, and dependency-injected
createApp so unit tests never touch external services.

CI: shared-deploy gains runtime-env/runtime-env-keys inputs (vercel deploy -e)
delivering runtime secrets and pinning ASSISTANT_ENV on the dev alias; preview
CORS narrowed from *.vercel.app to the aragon-app team scope
@tyhonchik tyhonchik force-pushed the app-266-customer-support-dialog-or-chatbot-p2 branch from 3d520bf to afcf807 Compare July 14, 2026 18:00
@tyhonchik tyhonchik marked this pull request as ready for review July 14, 2026 19:27
@tyhonchik tyhonchik marked this pull request as draft July 14, 2026 22:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant