Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/assistant-contracts-package-build.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@aragon/assistant-contracts": patch
---

Build the package to `dist/` (tsup CJS/ESM/types) so Node runtimes such as Vercel can require it; TypeScript source under `src/` is no longer the package entrypoint
5 changes: 5 additions & 0 deletions .changeset/assistant-contracts-support-intake.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@aragon/assistant-contracts": minor
---

Add the support-chat contracts: chat request/message schemas, collected-fields data part (required fields: summary and description — email is optional), issue create request/response, file confirm/delete requests and upload response, shared error shape, hard limits and the pinned Phase-2 docs-search result shape
5 changes: 5 additions & 0 deletions .changeset/assistant-support-intake.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@aragon/assistant": minor
---

Add the support-chat intake feature: streaming /chat pipeline (intent classification, field extraction, live collected-fields summary, optional email), idempotent /issues creation in Linear with attachment transfer at creation time, client-direct file uploads to Vercel Blob (token/confirm/delete endpoints, server-side magic-byte validation at confirm, daily orphan-cleanup cron), per-IP rate limiting via Upstash, structured step logs with Sentry, runtime env delivery for Vercel deployments and preview CORS narrowed to the aragon-app scope
3 changes: 3 additions & 0 deletions .github/workflows/app-preview.yml
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,9 @@ jobs:
workspace: "apps/assistant"
vault-prefix: "kv_assistant"
patch-root-directory: false
# Serverless functions only see runtime env vars (the .env file is build-time only).
runtime-env-keys: "AI_GATEWAY_API_KEY LINEAR_API_KEY LINEAR_TEAM_ID UPSTASH_REDIS_REST_URL UPSTASH_REDIS_REST_TOKEN BLOB_READ_WRITE_TOKEN CRON_SECRET SENTRY_DSN"
upload-sentry-source-maps: true

deploy:
needs: [changes, deploy-assistant]
Expand Down
5 changes: 5 additions & 0 deletions .github/workflows/assistant-development.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,8 @@ jobs:
workspace: "apps/assistant"
vault-prefix: "kv_assistant"
patch-root-directory: false
# dev.assistant is an aliased preview deployment, so VERCEL_ENV reports 'preview' at
# runtime — ASSISTANT_ENV pins the actual environment for src/lib/env.ts.
runtime-env: "ASSISTANT_ENV=development"
runtime-env-keys: "AI_GATEWAY_API_KEY LINEAR_API_KEY LINEAR_TEAM_ID UPSTASH_REDIS_REST_URL UPSTASH_REDIS_REST_TOKEN BLOB_READ_WRITE_TOKEN CRON_SECRET SENTRY_DSN"
upload-sentry-source-maps: true
3 changes: 3 additions & 0 deletions .github/workflows/assistant-production.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,6 @@ jobs:
workspace: "apps/assistant"
vault-prefix: "kv_assistant"
patch-root-directory: false
# VERCEL_ENV=production maps correctly at runtime; only the secrets need lifting.
runtime-env-keys: "AI_GATEWAY_API_KEY LINEAR_API_KEY LINEAR_TEAM_ID UPSTASH_REDIS_REST_URL UPSTASH_REDIS_REST_TOKEN BLOB_READ_WRITE_TOKEN CRON_SECRET SENTRY_DSN"
upload-sentry-source-maps: true
63 changes: 62 additions & 1 deletion .github/workflows/shared-deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,19 @@ on:
description: "Extra env lines appended to the workspace .env after secrets (e.g. per-run URL overrides)"
required: false
type: string
runtime-env:
description: "Runtime env vars for the deployment (multiline KEY=VALUE). The .env file only exists at build time; serverless functions need runtime vars passed via `vercel deploy -e`"
required: false
type: string
runtime-env-keys:
description: "Names of keys to lift from the prepared workspace .env into deployment runtime env vars — secrets stay in 1Password and never appear in workflow files"
required: false
type: string
upload-sentry-source-maps:
description: "Upload source maps using SENTRY_AUTH_TOKEN, SENTRY_ORG and SENTRY_PROJECT from the infrastructure vault"
required: false
type: boolean
default: false
outputs:
deploymentUrl:
description: "The URL of the Vercel deployment"
Expand Down Expand Up @@ -95,6 +108,14 @@ jobs:
ENV_OVERRIDES: ${{ inputs.env-overrides }}
run: printf '%s\n' "$ENV_OVERRIDES" >> ${{ inputs.workspace }}/.env

# Workspace packages that ship a built `dist/` (e.g. @aragon/assistant-contracts) must be
# compiled before `vercel build`: the lambda resolves them via package.json exports, and
# Node cannot execute the TypeScript source under packages/*/src. The `{dir}^...` filter
# selects only the dependencies of the workspace (never the workspace itself) and is a
# no-op for workspaces without buildable deps.
- name: Build workspace dependencies
run: pnpm --filter "{${{ inputs.workspace }}}^..." build

# Vercel never builds from git here: the artifact is built in this job and uploaded as-is
# (deploy --prebuilt). All vercel commands run from the repo root so Turbopack/Next treat the
# pnpm workspace root as the build root (pnpm symlinks point outside the workspace); project
Expand All @@ -118,8 +139,48 @@ jobs:
- name: Bundle Vercel application
run: pnpm vercel build ${{ env.VERCEL_ARGS }} --yes --token=${{ env.VERCEL_TOKEN }} --scope ${{ env.VERCEL_SCOPE }}

- name: Upload source maps to Sentry
if: inputs.upload-sentry-source-maps
run: |
: "${SENTRY_AUTH_TOKEN:?SENTRY_AUTH_TOKEN is required in the infrastructure vault}"
: "${SENTRY_ORG:?SENTRY_ORG is required in the infrastructure vault}"
: "${SENTRY_PROJECT:?SENTRY_PROJECT is required in the infrastructure vault}"
release=$(git rev-parse HEAD)
pnpm --dir "${{ inputs.workspace }}" exec sentry-cli sourcemaps inject "${GITHUB_WORKSPACE}/.vercel/output"
pnpm --dir "${{ inputs.workspace }}" exec sentry-cli sourcemaps upload --release "$release" --validate --wait "${GITHUB_WORKSPACE}/.vercel/output"

# The runtime `-e KEY=VALUE` args are built as a bash array in the same step (arrays don't
# survive GITHUB_ENV) so values containing spaces stay single arguments: the .env file
# prepared above only exists at build time, so anything a serverless function needs at
# runtime must be set as deployment env vars. Literal values come from `runtime-env`;
# `runtime-env-keys` lifts the named keys (1Password secrets) from the prepared .env,
# keeping values out of workflow files.
- name: Deploy to Vercel
run: pnpm vercel deploy --prebuilt ${{ env.VERCEL_ARGS }} ${{ inputs.env == 'production' && '--skip-domain' || '' }} --yes --token=${{ env.VERCEL_TOKEN }} --scope ${{ env.VERCEL_SCOPE }} > deployment-url.txt
env:
RUNTIME_ENV: ${{ inputs.runtime-env }}
RUNTIME_ENV_KEYS: ${{ inputs.runtime-env-keys }}
UPLOAD_SENTRY_SOURCE_MAPS: ${{ inputs.upload-sentry-source-maps }}
run: |
runtime_args=()
if [ "$UPLOAD_SENTRY_SOURCE_MAPS" = "true" ]; then
runtime_args+=(-e "SENTRY_RELEASE=$(git rev-parse HEAD)")
fi
while IFS= read -r line; do
[ -z "$line" ] && continue
runtime_args+=(-e "$line")
done <<< "$RUNTIME_ENV"
for key in $RUNTIME_ENV_KEYS; do
# Tolerate optional whitespace around '=' and at the end of the line — dotenv parsers
# accept `KEY = value`, so a hand-edited config entry must not be silently skipped by
# the runtime lift (and trailing whitespace must not defeat the quote stripping below).
value=$(grep -E "^[[:space:]]*${key}[[:space:]]*=" "${{ inputs.workspace }}/.env" | head -1 | cut -d= -f2- | sed -E 's/^[[:space:]]+//; s/[[:space:]]+$//')
value="${value%\"}"
value="${value#\"}"
if [ -n "$value" ]; then
runtime_args+=(-e "${key}=${value}")
fi
done
pnpm vercel deploy --prebuilt ${{ env.VERCEL_ARGS }} ${{ inputs.env == 'production' && '--skip-domain' || '' }} "${runtime_args[@]}" --yes --token=${{ env.VERCEL_TOKEN }} --scope ${{ env.VERCEL_SCOPE }} > deployment-url.txt

- name: Set output
id: set-deployment-url
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ out/

# production
build/
dist/

# misc
.DS_Store
Expand Down
6 changes: 3 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ This file is the team-shared agent entry point. `CLAUDE.md` imports it via `@AGE

- `apps/app/` — the Aragon App (app.aragon.org): all app source, configs (`next.config.mjs`, `tsconfig.json`, `jest.config.js`), `e2e/`, `docs/`, `scripts/`, `CHANGELOG.md`. Package name stays `@aragon/app`.
- `apps/assistant/` — the assistant service (assistant.aragon.org): Hono API behind the in-app support chat. Own Vercel project, continuous dev deploys from `main`, production released through its Version-PR flow (see `apps/assistant/README.md`).
- `packages/assistant-contracts/` — shared zod contracts between the assistant service and the assistant-chat widget. Domain-scoped by design, not a catch-all contracts package.
- `packages/assistant-contracts/` — shared zod contracts between the assistant service and the assistant-chat widget. Domain-scoped by design, not a catch-all contracts package. Built with `tsup` to `dist/` (CJS + ESM + `.d.ts`); Turbo `^build` and the shared-deploy workflow compile it before dependents type-check/test/deploy.
- `apps/*`, `packages/*` — reserved for future workspaces.
- Root — workspace infra only: `pnpm-workspace.yaml`, `turbo.json`, `biome.json`, `.github/`, `.husky/`, `.changeset/`, agent infra (`.agents/`, `.claude/`). Root `package.json` has no version; each workspace is versioned independently via changesets with per-package tags (`@aragon/app@1.17.0`) and release branches (`release/app/…`). See `apps/app/docs/projectDocs/release-process.md`.
- CI: workflows in `.github/workflows/` are grouped per app (`app-*.yml` for `apps/app`, `assistant-*.yml` for `apps/assistant`, `shared-*.yml` reusable). Root scripts proxy through `turbo run <task>`, so `pnpm type-check` etc. work from the repo root.
Expand Down Expand Up @@ -88,6 +88,6 @@ workspace — run them there: `cd apps/app && pnpm test:watch` (or `pnpm --filte
There are two deliberate execution paths in CI, and they must stay that way:

- **Graph tasks** (`type-check`, `lint:check`, `test:coverage`) run **from the repo root** via `turbo run …`. Turbo owns their cache/ordering, so they benefit from local caching and (later) the package graph.
- **`vercel build` and Playwright** run **inside `apps/app`** (`working-directory: apps/app`), because they need the app as the working directory — Vercel builds the artifact on the GH runner and uploads it with `deploy --prebuilt` (Vercel never builds from git here; see the comment in `.github/workflows/shared-deploy.yml`). This is why `turbo run build` is a non-caching passthrough (`cache: false`, no outputs): the real build is `vercel build`, not Turbo.
- **`vercel build` and Playwright** run from the repo root via `shared-deploy.yml` (workspace selected by input); Vercel builds the artifact on the GH runner and uploads it with `deploy --prebuilt` (Vercel never builds from git here). `turbo run build` stays a non-caching passthrough at the root (`cache: false`) because app/assistant artifacts are produced by `vercel build`, not Turbo — except workspace libraries that ship `dist/` (e.g. `@aragon/assistant-contracts`), which override `build` in their package `turbo.json` with `outputs: ["dist/**"]` and `cache: true`. `type-check` / `dev` depend on `^build` so those libraries are compiled before dependents run. `shared-deploy.yml` also runs `pnpm --filter "{<workspace>}^..." build` before `vercel build`.

Turbo caching is **local only** right now — no remote cache is wired (no `TURBO_TOKEN`/`TURBO_TEAM`). With a single package and a build that doesn't go through Turbo, a remote cache would buy nothing; revisit it once a second interdependent package (e.g. the assistant service) lands and Turbo's graph actually has work to memoize across machines. Task inputs are intentionally left at Turbo's default (hash all package files) rather than hand-narrowed globs — correctness over cache-hit-rate — and the root `biome.json` is listed in `globalDependencies` so lint caches invalidate when lint rules change.
Turbo caching is **local only** right now — no remote cache is wired (no `TURBO_TOKEN`/`TURBO_TEAM`). Task inputs are intentionally left at Turbo's default (hash all package files) rather than hand-narrowed globs — correctness over cache-hit-rate — and the root `biome.json` is listed in `globalDependencies` so lint caches invalidate when lint rules change.
30 changes: 24 additions & 6 deletions apps/assistant/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,35 @@ ASSISTANT_ENV=local
# Port for the local dev server
PORT=4000

# --- Secrets (1Password: kv_assistant_<env>) — required from the feature milestone onwards ---
# --- Optional overrides ---

# Per-IP rate limits (defaults live in src/lib/config.ts)
# ASSISTANT_RATE_LIMIT_RPM=10
# ASSISTANT_RATE_LIMIT_SESSIONS_PER_DAY=20

# --- Secrets (1Password: kv_assistant_<env>) ---

# Vercel AI Gateway key (dedicated key with a spend budget)
# AI_GATEWAY_API_KEY=
AI_GATEWAY_API_KEY=

# Linear service-bot API key (scoped to a single team)
# LINEAR_API_KEY=
LINEAR_API_KEY=

# Linear team the intake creates issues in: the dev/preview vaults carry the id of the
# dedicated test team, the production vault the real support team.
LINEAR_TEAM_ID=

# Upstash Redis (Vercel Marketplace integration)
# UPSTASH_REDIS_REST_URL=
# UPSTASH_REDIS_REST_TOKEN=
UPSTASH_REDIS_REST_URL=
UPSTASH_REDIS_REST_TOKEN=

# Vercel Blob store of the assistant project (intermediate storage for chat attachments;
# files move to Linear only when the ticket is created)
BLOB_READ_WRITE_TOKEN=

# Shared secret of the cron maintenance endpoints (/internal/*); Vercel Cron sends it
# automatically as a Bearer token when the env var is set on the project
CRON_SECRET=

# Sentry DSN of the assistant project
# SENTRY_DSN=
SENTRY_DSN=
16 changes: 12 additions & 4 deletions apps/assistant/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ The Aragon app (`apps/app`) reads `NEXT_PUBLIC_ASSISTANT_URL` from its own env c

Configuration layout:

- Non-secret per-environment config is a checked-in typed module (`src/lib/config.ts`) — Vercel functions receive no `.env` file at runtime, so file-based config lives in code and the runtime environment is derived from `ASSISTANT_ENV` (local) or the automatic `VERCEL_ENV`.
- Non-secret per-environment config is a checked-in typed module (`src/lib/config.ts`) — Vercel functions receive no `.env` file at runtime, so file-based config lives in code and the runtime environment is derived from `ASSISTANT_ENV` (local, and pinned by CI on the dev deployment where `VERCEL_ENV` reports `preview`) or the automatic `VERCEL_ENV`.
- Runtime secrets reach the deployed functions as per-deployment env vars: CI lifts them from the prepared `.env` and passes them to `vercel deploy -e` (`runtime-env-keys` in the deploy workflows).
- Build/dev-time variables are checked in under `config/.env.<env>` and copied to `.env.local` by `pnpm run setup <env>`.
- Secrets live exclusively in 1Password (`kv_assistant_<env>` vaults) and are propagated by CI — nothing is configured manually in the Vercel dashboard.
- Vercel project settings are config-as-code in `vercel.json` wherever Vercel supports it (framework, and later functions/crons/headers); the dashboard keeps only what cannot live in code: Root Directory, domains and the WAF toggle.
Expand All @@ -36,9 +37,16 @@ Rollback: re-deploy a previous deployment from the Vercel dashboard (instant rol

Manual steps, in order; all resulting credentials go to 1Password, never to the Vercel dashboard:

1. **Vercel**: create project `assistant` in the `aragon-app` scope, Root Directory = `apps/assistant`, domains `assistant.aragon.org` and `dev.assistant.aragon.org`, enable WAF. Marketplace → add Upstash Redis; copy `UPSTASH_REDIS_REST_URL`/`UPSTASH_REDIS_REST_TOKEN` to 1Password. Everything else stays out of the dashboard — project behavior belongs in `vercel.json`.
2. **1Password**: create vaults `kv_assistant_infra` (`VERCEL_PROJECT_ID`, `VERCEL_ORG_ID`, `VERCEL_TOKEN`) and `kv_assistant_{development,preview,production}` (`AI_GATEWAY_API_KEY`, `LINEAR_API_KEY`, `UPSTASH_REDIS_REST_URL`, `UPSTASH_REDIS_REST_TOKEN`, `SENTRY_DSN`).
1. **Vercel**: create project `assistant` in the `aragon-app` scope, Root Directory = `apps/assistant`, domains `assistant.aragon.org` and `dev.assistant.aragon.org`, enable WAF. Marketplace → add Upstash Redis; copy `UPSTASH_REDIS_REST_URL`/`UPSTASH_REDIS_REST_TOKEN` to 1Password. Storage → create a Blob store for the project; copy `BLOB_READ_WRITE_TOKEN` to 1Password (intermediate storage for chat attachments — files move to Linear only when the ticket is created; the daily `/internal/cleanup` cron sweeps abandoned blobs). Generate a random `CRON_SECRET` and store it in 1Password too. Everything else stays out of the dashboard — project behavior belongs in `vercel.json`.
2. **1Password**: create vaults `kv_assistant_infra` (`VERCEL_PROJECT_ID`, `VERCEL_ORG_ID`, `VERCEL_TOKEN`, `SENTRY_AUTH_TOKEN`, `SENTRY_ORG`, `SENTRY_PROJECT`) and `kv_assistant_{development,preview,production}` (`AI_GATEWAY_API_KEY`, `LINEAR_API_KEY`, `LINEAR_TEAM_ID`, `UPSTASH_REDIS_REST_URL`, `UPSTASH_REDIS_REST_TOKEN`, `BLOB_READ_WRITE_TOKEN`, `CRON_SECRET`, `SENTRY_DSN`). `LINEAR_TEAM_ID` points at the dedicated test team in the development/preview vaults and at the real support team in production. The Sentry token needs project release/file upload access; CI uses the three infrastructure values only to upload source maps.
3. **AI Gateway**: create a dedicated key for the assistant and set a spend budget on it.
4. **Sentry**: create project `assistant` in the existing org, copy the DSN, set error-rate alerts.
4. **Sentry**: create a dedicated Node/Hono project, copy its DSN and create an organization auth token with project release/file upload access. Store the organization and project slugs as `SENTRY_ORG` and `SENTRY_PROJECT` in the infrastructure vault rather than checking them into workflows. The service sends errors, traces, profiles, metrics and structured pipeline logs; request bodies, user/IP data, headers, cookies, query strings and free-text exception messages are stripped in code. CI associates telemetry with the deployed git SHA and uploads source maps before deployment. Configure alerts for new issues, error-rate regression and p95 transaction-duration regression. After deploying development, verify the full pipeline once:

```sh
curl -i -H "Authorization: Bearer $CRON_SECRET" \
https://dev.assistant.aragon.org/internal/debug-sentry
```

Expect HTTP 500, then confirm the `development` event, `assistant.debug_sentry` log, `assistant.debug_counter` metric, trace/profile and de-minified stack in Sentry. The endpoint is authenticated and returns 404 in production.
5. **Linear**: create a service-bot API key scoped to the target team; ensure labels `feedback`, `bug`, `docs-gap` exist; pick the test team/label used by the LLM smoke checks.
6. **GitHub**: review required checks after the CI paths filters (skipped jobs report as passing).
28 changes: 28 additions & 0 deletions apps/assistant/jest.config.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
'use strict';
const {
createNodeConfig,
createTsJestTransform,
} = require('../../jest.config.base');

/** @type {import('@jest/types').Config.InitialOptions} */
const config = createNodeConfig({
coveragePathIgnorePatterns: ['src/dev.ts', 'src/test/'],
});

// Silences the observability stdout/stderr transport so test output stays readable.
config.setupFilesAfterEnv = ['<rootDir>/src/test/setup.ts'];

// Resolve contracts to TypeScript source so jest does not depend on a prior `dist/` build.
config.moduleNameMapper = {
'^@aragon/assistant-contracts$':
'<rootDir>/../../packages/assistant-contracts/src/index.ts',
};

// ai (+ @ai-sdk) and file-type (+ its tokenizer graph) ship ESM only: compile them to CJS for
// jest. The optional `.pnpm/<pkg>/node_modules/` segment matches pnpm's nested store layout.
config.transform = createTsJestTransform({ allowJs: true });
config.transformIgnorePatterns = [
'node_modules/(?!(?:\\.pnpm/[^/]+/node_modules/)?(ai|@ai-sdk|@workflow|file-type|strtok3|token-types|uint8array-extras|@tokenizer|@borewit|peek-readable)(/|$))',
];

module.exports = config;
6 changes: 0 additions & 6 deletions apps/assistant/jest.config.js

This file was deleted.

Loading
Loading