Skip to content
Merged
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
2 changes: 1 addition & 1 deletion .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
{
"name": "pagent",
"source": "./",
"description": "Generative UI for terminal-bound AI agents — render interactive browser UIs (forms, pickers, dashboards, confirmations) via the A2UI protocol. Ships a stdio MCP and skill, with hosted rendering at agent-ui-session.vercel.app or self-hosted via PAGENT_URL.",
"description": "Generative UI for terminal-bound AI agents — render interactive browser UIs (forms, pickers, dashboards, confirmations) via the A2UI protocol. Ships a stdio MCP and skill, with hosted rendering at pagent.vercel.app or self-hosted via PAGENT_URL.",
"category": "productivity",
"tags": ["generative-ui", "agentic-ui", "a2ui", "mcp"]
}
Expand Down
2 changes: 1 addition & 1 deletion .claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "pagent",
"version": "0.0.1",
"description": "Generative UI for terminal-bound AI agents — render interactive browser UIs (forms, pickers, dashboards, confirmations) via the A2UI protocol. Ships a stdio MCP and skill, with hosted rendering at agent-ui-session.vercel.app or self-hosted via PAGENT_URL.",
"description": "Generative UI for terminal-bound AI agents — render interactive browser UIs (forms, pickers, dashboards, confirmations) via the A2UI protocol. Ships a stdio MCP and skill, with hosted rendering at pagent.vercel.app or self-hosted via PAGENT_URL.",
"author": "Alexandro T. Netto",
"homepage": "https://github.com/blockful/pagent",
"license": "MIT",
Expand Down
75 changes: 75 additions & 0 deletions .github/workflows/vercel-alias-pagent.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
name: Pin pagent.vercel.app to latest production

# Re-alias pagent.vercel.app to whichever pagent project deployment Vercel
# has just promoted to production. Required because the bare pagent.vercel.app
# subdomain is owned by another team's project at the project-domain layer
# (Vercel rejects adding it as our project's auto-tracking domain), so we
# can serve from it via deployment aliases but it does NOT auto-update on
# new prod deploys. This workflow does the re-alias for us.
#
# Setup: add a VERCEL_TOKEN secret with team-scope access to the ful team.
# Without the secret, the workflow no-ops loudly (does not fail).

on:
push:
branches: [main]
workflow_dispatch:

concurrency:
group: vercel-alias-pagent
cancel-in-progress: false

jobs:
alias:
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: read
env:
VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
VERCEL_PROJECT_ID: prj_JhOxWsbBLRb1Sdo19Thbz4eHOyj7
VERCEL_TEAM_ID: team_zDJQ6yrYdOu79gJBmUIBVYCV
ALIAS: pagent.vercel.app
steps:
- name: Skip if VERCEL_TOKEN is unset
id: gate
run: |
if [ -z "$VERCEL_TOKEN" ]; then
echo "::warning::VERCEL_TOKEN secret is unset — skipping auto-alias. Add it under Settings → Secrets → Actions to enable."
echo "skip=true" >> "$GITHUB_OUTPUT"
else
echo "skip=false" >> "$GITHUB_OUTPUT"
fi

- name: Wait for the production deployment to be READY
if: steps.gate.outputs.skip == 'false'
id: wait
run: |
for i in $(seq 1 30); do
JSON=$(curl -s -H "Authorization: Bearer $VERCEL_TOKEN" \
"https://api.vercel.com/v6/deployments?projectId=$VERCEL_PROJECT_ID&teamId=$VERCEL_TEAM_ID&target=production&limit=1")
STATE=$(echo "$JSON" | jq -r '.deployments[0].state // "unknown"')
URL=$(echo "$JSON" | jq -r '.deployments[0].url // ""')
echo "attempt $i: state=$STATE url=$URL"
if [ "$STATE" = "READY" ] && [ -n "$URL" ]; then
Comment on lines +50 to +54
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Filter aliased deployment to current push SHA

This loop aliases whichever deployment is currently returned by target=production&limit=1, but it never verifies that deployment belongs to the commit that triggered this run. On a fast push to main, the new Vercel deployment may not exist yet, so the API can return the previous production deployment in READY state, causing the job to exit successfully while re-pointing pagent.vercel.app to stale code. Please gate on a deployment field tied to ${{ github.sha }} (or wait until that SHA appears) before accepting READY.

Useful? React with 👍 / 👎.

echo "url=$URL" >> "$GITHUB_OUTPUT"
exit 0
fi
if [ "$STATE" = "ERROR" ] || [ "$STATE" = "CANCELED" ]; then
echo "::error::Latest production deployment is $STATE — nothing to alias."
exit 1
fi
sleep 10
done
echo "::error::Timed out waiting for production deployment to be READY."
exit 1

- name: Install Vercel CLI
if: steps.gate.outputs.skip == 'false'
run: npm install -g vercel@latest

- name: Re-alias $ALIAS to ${{ steps.wait.outputs.url }}
if: steps.gate.outputs.skip == 'false'
run: |
vercel alias set "${{ steps.wait.outputs.url }}" "$ALIAS" \
--token "$VERCEL_TOKEN" --scope "$VERCEL_TEAM_ID"
20 changes: 10 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
Hosted UI rendering for terminal-bound AI agents. The agent emits an A2UI surface to this service, prints a short URL, and reads the user's interactions back via API.

- **Live API:** https://pagent.up.railway.app
- **Live renderer:** https://agent-ui-session.vercel.app
- **Live renderer:** https://pagent.vercel.app

See [PRD.md](./PRD.md) for the design and [HANDOFF.md](./docs/HANDOFF.md) for build context.

Expand Down Expand Up @@ -120,7 +120,7 @@ You should see `pagent` listed with `show_ui` and `check_result` tools. The plug

> "Use the pagent skill to ask me my favorite color via a UI form."

The agent calls `show_ui`, prints a URL (hosted at `https://agent-ui-session.vercel.app`), you submit, and the conversation continues.
The agent calls `show_ui`, prints a URL (hosted at `https://pagent.vercel.app`), you submit, and the conversation continues.

**Point at a different service?** Set `PAGENT_URL` before launching Claude. By default the MCP talks to `https://pagent.up.railway.app`.

Expand Down Expand Up @@ -196,7 +196,7 @@ To bypass in an emergency: `git push --no-verify` (don't make this a habit).
1. Create a new Railway service from this repo.
2. Set **Root Directory** to `apps/api` so Railway picks up the railway.json.
3. Set environment variables (see `apps/api/.env.example`):
- `PUBLIC_URL` — the Vercel URL of `apps/web` (e.g. `https://agent-ui-session.vercel.app`). Used in `show_ui` responses. **Required in production.** Boot fails loudly if missing.
- `PUBLIC_URL` — the Vercel URL of `apps/web` (e.g. `https://pagent.vercel.app`). Used in `show_ui` responses. **Required in production.** Boot fails loudly if missing.
- `ALLOWED_ORIGINS` — comma-separated origins allowed to call the API (set to your Vercel URL). **Required in production.** API boot fails loudly if missing.
- `PORT` — Railway sets this automatically; the server reads it.
- `PAGE_TTL_MS` — optional; default 30 minutes.
Expand Down Expand Up @@ -299,13 +299,13 @@ in Grafana from the trace and log streams.

### Common failure modes

| Symptom | Likely cause | Where to look | First response |
| ------------------------------------------------------------------- | -------------------------------------------------------- | ------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------- |
| `GET /health` → 503 | Postgres unreachable | Supabase status page; Railway DB env vars | Check Supabase dashboard. If the DB is up but the env var was rotated, restore `DATABASE_URL` in Railway and redeploy. |
| Spike of 429s on `POST /new` | Per-IP rate limit hit (default 30 req / 60 s) | Railway logs — group by client IP | Legit spike: bump `RATE_LIMIT_MAX` in Railway env and restart (no redeploy needed). Abuse: block at the network edge. |
| 413 on `POST /new` | Request body > 256 KB | Log field `error: payload_too_large` | If a real use case, raise `MAX_BODY_BYTES` in `apps/api/app.ts` (code change + redeploy). Otherwise it's spam; ignore. |
| CORS errors in the browser console at `agent-ui-session.vercel.app` | `ALLOWED_ORIGINS` does not include the renderer's origin | Browser DevTools → Network → failing preflight | Add the missing origin to `ALLOWED_ORIGINS` in Railway env and restart the service. |
| Boot failure with `ZodError` in Railway logs | A required env var is missing | Railway logs (the process exits before it binds) | Read the Zod validation error — it names the missing field. Usually `PUBLIC_URL` or `ALLOWED_ORIGINS`. Set it in Railway, then redeploy. |
| Symptom | Likely cause | Where to look | First response |
| --------------------------------------------------------- | -------------------------------------------------------- | ------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------- |
| `GET /health` → 503 | Postgres unreachable | Supabase status page; Railway DB env vars | Check Supabase dashboard. If the DB is up but the env var was rotated, restore `DATABASE_URL` in Railway and redeploy. |
| Spike of 429s on `POST /new` | Per-IP rate limit hit (default 30 req / 60 s) | Railway logs — group by client IP | Legit spike: bump `RATE_LIMIT_MAX` in Railway env and restart (no redeploy needed). Abuse: block at the network edge. |
| 413 on `POST /new` | Request body > 256 KB | Log field `error: payload_too_large` | If a real use case, raise `MAX_BODY_BYTES` in `apps/api/app.ts` (code change + redeploy). Otherwise it's spam; ignore. |
| CORS errors in the browser console at `pagent.vercel.app` | `ALLOWED_ORIGINS` does not include the renderer's origin | Browser DevTools → Network → failing preflight | Add the missing origin to `ALLOWED_ORIGINS` in Railway env and restart the service. |
| Boot failure with `ZodError` in Railway logs | A required env var is missing | Railway logs (the process exits before it binds) | Read the Zod validation error — it names the missing field. Usually `PUBLIC_URL` or `ALLOWED_ORIGINS`. Set it in Railway, then redeploy. |

### Rollback

Expand Down
4 changes: 2 additions & 2 deletions apps/api/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,15 @@ PORT=8787

# The public URL where users open pages in their browser.
# In dev: http://localhost:8788 (the Vite renderer, NOT the API on 8787).
# In prod: your Vercel deployment URL (e.g. https://agent-ui-session.vercel.app).
# In prod: your Vercel deployment URL (e.g. https://pagent.vercel.app).
# Required when NODE_ENV=production — boot will fail loudly otherwise.
PUBLIC_URL=http://localhost:8788

# How long pages live, in milliseconds (default 30 minutes).
PAGE_TTL_MS=1800000

# Comma-separated list of origins allowed to call the API. Leave unset for dev (allows all).
# Required when NODE_ENV=production — boot will fail loudly otherwise. Set to your Vercel URL (e.g. https://agent-ui-session.vercel.app).
# Required when NODE_ENV=production — boot will fail loudly otherwise. Set to your Vercel URL (e.g. https://pagent.vercel.app).
ALLOWED_ORIGINS=

# Supabase Postgres connection string (Session pooler, port 5432).
Expand Down
2 changes: 1 addition & 1 deletion apps/api/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ app.use(
// has no frames to embed and DENY is more restrictive.
xFrameOptions: 'DENY',
// Browsers default Cross-Origin-Resource-Policy to same-origin which would
// block the renderer at agent-ui-session.vercel.app from reading API responses at
// block the renderer at pagent.vercel.app from reading API responses at
// pagent.up.railway.app. CORS already gates cross-origin reads explicitly.
crossOriginResourcePolicy: 'cross-origin',
// Defaults are fine for everything else (HSTS, X-Content-Type-Options
Expand Down
6 changes: 3 additions & 3 deletions apps/api/schemas.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -183,8 +183,8 @@ describe('envSchema', () => {
const r = envSchema.safeParse({
DATABASE_URL: 'x',
NODE_ENV: 'production',
ALLOWED_ORIGINS: 'https://agent-ui-session.vercel.app',
PUBLIC_URL: 'https://agent-ui-session.vercel.app',
ALLOWED_ORIGINS: 'https://pagent.vercel.app',
PUBLIC_URL: 'https://pagent.vercel.app',
});
expect(r.success).toBe(true);
});
Expand Down Expand Up @@ -216,7 +216,7 @@ describe('envSchema', () => {
DATABASE_URL: 'x',
NODE_ENV: 'production',
ALLOWED_ORIGINS: 'https://a.com',
PUBLIC_URL: 'https://agent-ui-session.vercel.app',
PUBLIC_URL: 'https://pagent.vercel.app',
});
expect(r.success).toBe(true);
});
Expand Down
4 changes: 2 additions & 2 deletions apps/api/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,15 +72,15 @@ export const envSchema = z.preprocess(
code: 'custom',
path: ['ALLOWED_ORIGINS'],
message:
'ALLOWED_ORIGINS is required in production. Set it to a comma-separated list of origins permitted to call the API (e.g. https://agent-ui-session.vercel.app).',
'ALLOWED_ORIGINS is required in production. Set it to a comma-separated list of origins permitted to call the API (e.g. https://pagent.vercel.app).',
});
}
if (cfg.NODE_ENV === 'production' && !cfg.PUBLIC_URL) {
ctx.addIssue({
code: 'custom',
path: ['PUBLIC_URL'],
message:
'PUBLIC_URL is required in production. Set it to the renderer URL (e.g. https://agent-ui-session.vercel.app).',
'PUBLIC_URL is required in production. Set it to the renderer URL (e.g. https://pagent.vercel.app).',
});
}
}),
Expand Down
2 changes: 1 addition & 1 deletion apps/web/home.ts
Original file line number Diff line number Diff line change
Expand Up @@ -532,7 +532,7 @@ class HomePage extends LitElement {
</div>
<div>
<span class="prompt">↳</span
><span class="url">https://agent-ui-session.vercel.app/4f2a…b13c</span
><span class="url">https://pagent.vercel.app/4f2a…b13c</span
><span class="caret"></span>
</div>
</div>
Expand Down
2 changes: 1 addition & 1 deletion docs/RELEASING.md
Original file line number Diff line number Diff line change
Expand Up @@ -157,5 +157,5 @@ Users who already installed the plugin will pick up the reverted `main` on their

- Update `README.md` quickstart if any new commands or env-vars shipped.
- Confirm Railway auto-deployed from `main`: `curl https://pagent.up.railway.app/health`.
- Confirm Vercel auto-deployed from `main`: open `https://agent-ui-session.vercel.app` and check the page loads.
- Confirm Vercel auto-deployed from `main`: open `https://pagent.vercel.app` and check the page loads.
- If the Railway or Vercel deploys did not trigger automatically, redeploy manually from their dashboards.
2 changes: 1 addition & 1 deletion docs/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -525,7 +525,7 @@ components:
description: >
Short URL for the user to open in a browser. Points to the
Pagent renderer with this page's ID.
example: https://agent-ui-session.vercel.app/c0f2ec161aac8b1a8d26222f45ca812d
example: https://pagent.vercel.app/c0f2ec161aac8b1a8d26222f45ca812d
expires_at:
type: integer
description: Unix epoch milliseconds when this page will be deleted.
Expand Down
2 changes: 1 addition & 1 deletion requests.prod.http
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
### Hits the deployed services — no local dev server needed.

@apiHost = https://pagent.up.railway.app
@viewHost = https://agent-ui-session.vercel.app
@viewHost = https://pagent.vercel.app

###############################################################################
# 1. Health check
Expand Down
Loading