feat(APP-266): support intake pipeline, Linear integration and guardrails for the assistant service (p2)#1224
Draft
tyhonchik wants to merge 1 commit into
Draft
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
44bc09f to
44ec6c7
Compare
44ec6c7 to
f25f0a3
Compare
f25f0a3 to
aabe7b8
Compare
aabe7b8 to
b4dca02
Compare
b4dca02 to
843b86c
Compare
843b86c to
1c99c34
Compare
1c99c34 to
92802b8
Compare
92802b8 to
0a4d477
Compare
0a4d477 to
806563a
Compare
806563a to
79bca78
Compare
79bca78 to
d38e548
Compare
d38e548 to
9f480a4
Compare
|
E2E results (preview) Smoke
|
dbd0ed5 to
4cbed4c
Compare
4cbed4c to
9fe9cfa
Compare
9fe9cfa to
4a639ab
Compare
4a639ab to
67991fc
Compare
67991fc to
31f8ae0
Compare
31f8ae0 to
64dd4d1
Compare
64dd4d1 to
3d520bf
Compare
…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
3d520bf to
afcf807
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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/chatstreams a deterministic intake conversation (classify intent → extract fields → respond),/issuescreates 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/redisdirectly, both rate limits run on@upstash/ratelimit, CORS ishono/corswith an origin callback and model fallback is native AI Gateway — no hand-rolled KV/rate-limiter/CORS/fallback abstractions.Changes
honopreset resolvessrc/app.tsas the serverless entry and requires the Hono app as the default export — this is what brokeassistant-livid.vercel.app/health.app.tsnow default-exports the constructed app (real dependencies stay lazy) andsrc/index.tsis deleted so there is a single entry candidate.@aragon/assistant-contracts): chat/issue/file/error schemas, thedata-collectedFieldsdata part driving the live summary, sharedassistantLimitsand the pinned Phase-2docSearchResultSchema.createIssueRequestSchema = chatRequestSchema— the server is stateless and re-extracts from the submitted transcript. Email is optional: asked for softly, never blocks creation./chatpipeline on AI SDK v7 via the AI Gateway (primaryanthropic/claude-haiku-4.5):generateText+Output.objectclassify + extract on every turn, live-summary data part,streamTextreply 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 viaproviderOptions.gateway.models; step logs record the actually served model.POST /files/tokenissues a client token (@vercel/blob/clienthandleUpload, validating the client-proposedassistant/{sessionId}/{fileId}/{filename}pathname), the browser uploads directly to Blob,POST /files/confirmpins 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/:fileIdis idempotent with an LREM-guarded slot release (exactly once under races) and best-effort blob delete. Linear sees files only atPOST /issues(re-validated, upload failure → 502 + claim release so retry works); after success the blobs are deleted. A daily cronGET /internal/cleanup(BearerCRON_SECRET) sweeps orphaned blobs older than 24h.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./issuesidempotency: atomicSET NXclaim 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.@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 inlib/rateLimit.ts.@upstash/redis(Redis.fromEnv({automaticDeserialization: false})— keepsLREMbyte-exact and avoids double-parse);sessionStore.tsstays the single session API including the atomic file-slot reservation. CORS ishono/corswith a ~15-line origin resolver (lib/cors.ts): exact origins +*-prefix suffix patterns, preview scope narrowed to*-aragon-app.vercel.app.src/instrument.ts(before the app), source-map upload wired intoshared-deploy.yml, unified JSON error shape viaapp.onError.createApp(overrides)with lazy getters (redis, sessionStore, linear, chatModel, blobStore) — tests never touch Upstash/Linear/Gateway, and/healthkeeps working with no secrets configured./issuesidempotency + double-POST race + claim release, file-slot and claim races on a Redis fake (which emulates the two@upstash/ratelimitLua 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.shared-deploy.ymlgainsruntime-env/runtime-env-keysinputs feedingvercel deploy -e(the.envfile only exists at build time); assistant deploys lift the 1Password secrets (+BLOB_READ_WRITE_TOKEN,CRON_SECRET) to runtime, dev pinsASSISTANT_ENV=development.Note
kv_assistant_*, includingLINEAR_TEAM_ID(test team in development/preview, real support team in production),BLOB_READ_WRITE_TOKENandCRON_SECRET.+++ Transcriptcollapsible syntax is unverified against a live workspace — if it renders literally, the fallback is cosmetic only; check the first real ticket.config.ts(anthropic/claude-haiku-4.5primary,google/gemini-2.5-flashfallback) — final pick still open, one-line change.🤖 Generated with Claude Code