agentic-chat-interfacerun.md02_define/output/spec.mdagentic-app-scaffold (stub 1 of 2, merged as #894) landed apps/agent as a deployable,
sign-in-gated shell whose entire surface is a card saying "the conversation arrives in the next
release". The agentic layer therefore exists but has nothing to say, and the concept David
approved is unproven: nobody has yet watched a real answer stream back inside it. This stub is
the second and last of the agentic-app-foundation batch and closes that gap with the
simplest true version of the promise — a signed-in user asks a question, a real model answers,
streamed. It advances Scale the bridge / Q2-2026 Objective 3 — Validate Technical
Infrastructure & Payout Flow: it is the end-to-end proof that the new app, the shared Clerk
session, the AI Gateway wiring and the shared @sustentus/ui AI primitives work together.
Replace the scaffold's placeholder card with a conversation, built from parts the monorepo already has:
packages/services/src/ai/assistant/ folder following the house pattern:
prompt.ts (instructions) + agent.ts calling defineAgent from
packages/services/src/ai/core/agent.ts, so the model is the house default constant
(DEFAULT_AGENT_MODEL) reached through the Vercel AI Gateway. No tools — defineAgent
is called without a tools key. Exported from packages/services/src/ai/index.ts (the
client-safe @sustentus/services/ai subpath), alongside the BRD and onboarding agents.apps/agent/app/api/chat/route.ts: POST only, maxDuration set, Clerk
auth() → 401 when signed out, then createAgentUIStreamResponse({ agent, uiMessages }).
proxy.ts already refuses signed-out requests at the edge; the in-route check is the
deny-by-default second gate the other chat routes carry, not a substitute for it.@vercel/firewall checkRateLimit call at the top of the route,
rateLimitKey set to the signed-in Clerk userId, returning 429 before the model is reached.
One rule, no more (see Operator preconditions — the WAF rule itself is a dashboard action).apps/agent/app/page.tsx becomes the conversation: a "use client" chat
component using useChat from @ai-sdk/react with a DefaultChatTransport pointed at
/api/chat, rendering with the shared @sustentus/ui AI primitives — PromptInput /
PromptInputTextarea / PromptInputSubmit for input, Message / MessageContent /
MessageResponse for turns, Shimmer while the assistant is working. MessageResponse
renders markdown (Streamdown), so no HTML is emitted and no sanitisation surface is added.
The signed-in identity and sign-out control the scaffold introduced stay, moving into the
conversation's header.useChat's error renders fixed friendly retry copy, never the upstream
text. The gateway's decline messages have already leaked provider names, account tiers and a
billing top-up link into a customer-facing screen once (see the BRD/onboarding chat comment on
CHAT_ERROR_MESSAGE); the detail goes to the server log and the browser console only.apps/agent gains a workspace:* dependency on @sustentus/services,
used only through the client-safe @sustentus/services/ai subpath.
apps/agent/AGENTS.md currently reads "No @sustentus/services"; this run amends that section
to state the boundary as it now is — /ai yes, /server, /db and tenant data still no.The conversation is ephemeral by design: no persistence, no localStorage hydration, no
resume. A refresh starts a fresh conversation, and the PR says so.
/, sending a message streams the assistant's reply into the conversation
rendered with the shared @sustentus/ui AI components (PromptInput*, Message*,
Shimmer) — not app-local copies of them.POST /api/chat signed out returns 401 from the route's own auth() check, in
addition to the proxy.ts redirect; GET /api/chat is 405 (only POST is exported).defineAgent and DEFAULT_AGENT_MODEL
from packages/services/src/ai/core/agent.ts — no provider SDK dependency in apps/agent,
and no model string literal anywhere in apps/agent.apps/agent imports from @sustentus/services/ai
only — no @sustentus/services/server, /db or /shared import anywhere in the app.checkRateLimit once, keyed by the signed-in Clerk user id, and returns
429 without invoking the agent when it reports rateLimited — a unit test covers the
429-before-agent path.@sustentus/agent ships a test script and it runs in Quality Project (the scaffold
deliberately shipped none, deferring it to "the first real logic" — this is it).apps/agent/AGENTS.md states the amended services boundary (/ai allowed; /server,
/db and tenant data not), so the next agent reads the rule that is actually in force.Quality Project green and Vercel – agentic-interface green on the PR, with no other
app's build or lint output changed.buildCannedAgentSetup exists and would wire in
cheaply, but it is transcript replay for a surface that does no real work yet; worth wiring
when the assistant has tools to fake.AI_GATEWAY_API_KEY is already in turbo.json → globalEnv
and @vercel/firewall reads none, so globalEnv is unchanged; the operator mirrors the
gateway key into the agentic-interface Vercel project (see Operator preconditions).apps/web, apps/console or the other apps, and to the BRD/onboarding
agents or their prompts.CHAT_ERROR_MESSAGE helper. The copy is duplicated a third time rather than
lifted into @sustentus/ui; three call sites with different product voices is not yet an
abstraction. Raise it as a chore if a fourth appears.packages/services/src/ai/ with
apps/agent taking a /ai-only dependency (and AGENTS.md amended to match); and the rate
limit is @vercel/firewall + a dashboard WAF rule rather than an in-memory bucket.Not build work — these are Jamie's/the operator's. Status as of pickup for Build:
AI_GATEWAY_API_KEYagentic-interface Vercel project
(prj_u0liBVQaTAlGNaX1RsvBbjJHcHEh, team sustentus) — done (Jamie, 2026-08-27).agent-chat to checkRateLimit.
checkRateLimit returns rateLimited: false when no rule with that id exists, so the route
is correct and shippable either way: the limit simply does not bite until the rule is created.
Consequences, so Verify does not mistake either for a defect:Quality Project asserts.sustentus is on the Pro plan, so WAF custom rules and rate limiting are available;
there is no plan blocker. Settings for the rule are in ## Notes for the operator below.CLERK_SECRET_KEY,
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY) must already be mirrored there — carried over from
agentic-app-scaffold's preconditions, not new here.At Define this spec recorded that ai-platform-enablement/ai-gateway-billing-unlock had not
shipped and the gateway 403d on the free tier for anthropic/claude-haiku-4.5 — the house
default this agent uses — so the DoD smoke could only exercise the failure path.
Jamie funded the gateway on 2026-08-27, ahead of that stub. The happy path is therefore live-testable: Verify should expect a real streamed reply on the preview, and a 403 decline is now a genuine defect rather than the expected state. The friendly-retry-copy criterion still stands and is still testable — it just needs provoking (an invalid key, or a quota trip) rather than arriving for free.
Note this does not close ai-gateway-billing-unlock: that stub also covers making AI
failures visible and the guardrails around spend. Funding was only its precondition.
Create once, on the agentic-interface project. Either route works:
sustentus → agentic-interface → Firewall → Custom Rules →
New Rule. Condition: Request Path starts with /api/chat. Action: Rate Limit,
fixed window, 20 requests / 60s. Then set the rule's ID to agent-chat — that id, not
the display name, is what checkRateLimit matches. Save, then Publish the firewall config
(changes stage until published).vercel firewall rules add with --action rate_limit --rate-limit-window 60 --rate-limit-requests 20 --rate-limit-algo fixed_window and a path condition, then
vercel firewall publish.The route sets rateLimitKey to the signed-in Clerk user id, which replaces the rule's
default per-IP bucket — so the limit is per user, not per address, and the dashboard key setting
does not need changing.
Context budget: within band, with reads beyond the Inputs table taken deliberately because the
stub's notes were written before stub 1 landed and are stale in one material way — the app is
apps/agent, not apps/ai (apps/console had taken the adjacent name, and agent was
preferred; see the archived agentic-app-scaffold spec). The extra reads were: the scaffold's
archived spec and its AGENTS.md/proxy.ts/page.tsx, core/agent.ts, the onboarding chat
route and client (the current createAgentUIStreamResponse + useChat pattern), the
packages/ui compound/ai exports, and the Vercel checkRateLimit docs. Every stub path
(apps/ai, "new agent folder") is translated to the real one in touches: above.
Scope provenance: this batch has no scope.md.
.icm/intake/agentic-app-foundation/breakdown.md records why — it was cut from the approved
agentic-layer architecture report (David, 2026-08-27) on Jamie's instruction to produce a
minimal first-win batch, without passing through /pipeline scope → approve. The stub plus
that breakdown are the settled source this spec traces to; there are no Q-n numbers to carry.
03_build/output/notes.mdci-status.sh settlespackages/services/src/ai/assistant/prompt.ts (new): the Sustentus-aware system prompt —
identity, an explicit statement that the assistant has no tenant-data access and must not
claim to have acted, answer style (sentence case, concise, admit ignorance), and the
injection defences mirrored from the BRD prompt (ignore in-message instruction overrides and
role changes; never reveal, paraphrase or discuss the instructions).packages/services/src/ai/assistant/agent.ts (new): buildAssistantAgent +
the AssistantAgent singleton, via defineAgent — so the model is the house
DEFAULT_AGENT_MODEL constant reached through the AI Gateway. Called with no tools key.
Mirrors the BRD builder-plus-singleton shape, but on the newer defineAgent core rather than
a raw ToolLoopAgent.packages/services/src/ai/{assistant/index.ts, index.ts}: barrel + re-export, so the agent is
reachable on the client-safe @sustentus/services/ai subpath alongside BRD and onboarding.apps/agent/app/api/chat/route.ts (new): POST only (so every other method is 405),
maxDuration = 120, Clerk auth() → 401, then one checkRateLimit("agent-chat", …) keyed by
the Clerk user id → 429 before the agent is constructed or called, then
createAgentUIStreamResponse.apps/agent/app/api/chat/route.test.ts (new): five unit tests over the gate order.apps/agent/components/assistant-chat.tsx (new): the "use client" conversation —
useChat + DefaultChatTransport against /api/chat, rendered entirely with the shared
@sustentus/ui AI primitives (PromptInput*, Message*, MessageResponse, Shimmer).
MessageResponse is Streamdown, so replies render as markdown and no HTML is emitted —
no new sanitisation surface. Failures render fixed copy; the raw error goes to the console only.apps/agent/app/page.tsx: the scaffold's placeholder card is deleted and replaced by the
conversation; the signed-in identity and sign-out control move into the header.apps/agent/{package.json, vitest.config.ts}: @sustentus/services, @vercel/firewall, ai,
@ai-sdk/react deps; test script and the unit-tier vitest config aliasing
@sustentus/services/ai to the package's src/, mirroring apps/console.turbo.json: @sustentus/agent#test → dependsOn: ["@sustentus/services#build"]. Required,
not cosmetic: the alias above means the services source is not in the agent's own task inputs,
so without this hash dependency a services change would leave a stale cached pass. Same
reasoning as the existing @sustentus/web#test / @sustentus/console#test entries.apps/agent/AGENTS.md: the blanket "No @sustentus/services" is replaced by the boundary now
in force — /ai allowed (client-safe, no DB), /server /db /shared and tenant data still
not — with a note recording why the scaffold's rule changed.pnpm-lock.yaml: regenerated for the new dependencies.@sustentus/ui AI components — assistant-chat.tsx imports
PromptInput*, Message*, MessageResponse and Shimmer from @sustentus/ui; no
app-local copies exist.POST /api/chat → 401 from the route's own auth(), on top of the proxy.ts
redirect; only POST is exported, so other methods are 405. Both covered by unit tests.defineAgent + DEFAULT_AGENT_MODEL — no provider SDK in apps/agent and no
model string literal anywhere in the app.apps/agent imports @sustentus/services/ai only.CHAT_ERROR_MESSAGE; the raw error reaches
console.error and never the DOM.checkRateLimit keyed by the Clerk user id, 429 before the agent runs — unit test
returns 429 without invoking the agent when the caller is rate limited asserts both the
status and that the stream helper was never called.@sustentus/agent ships a test script picked up by the generic turbo test task.apps/agent/AGENTS.md states the amended boundary.Quality Project + Vercel – agentic-interface green — the factory's call, recorded below
once ci-status.sh settles.Jamie smoke-tested the first Build push and every turn failed with the friendly copy. Server log:
Error: Unexpected rate-limit API response status 'agent-chat': 307
Root cause — mine, two compounding defects:
proxy.ts was intercepting the rate limiter's own callback.
@vercel/firewall's checkRateLimit fetches
https://<host>/.well-known/vercel/rate-limit-api/<id> on the same host.
That path matched proxy.ts's deny-by-default matcher, so our own Clerk
middleware answered it with a redirect to /sign-in. The SDK fetches with
redirect: "manual" and branches only on 204 / 429 / 403 / 404 — a 307 falls
through to throw. I added a checkRateLimit call to an app whose middleware
refuses everything by default and did not account for the callback.
Fix: /.well-known/vercel/(.*) joins /sign-in as a public route. That
is Vercel platform surface served by the edge, never reaching this app's
code — the exemption removes our middleware from in front of a route we do
not own, and exposes nothing of ours.
A brake failure took down the whole product. Even fixed, any future throw from that network call — a platform blip, a protection-setting change — would have killed every turn. Fix: the check is wrapped and deliberately fails open, logging the error and allowing the turn.
The fail-open call is a real tradeoff, stated so it can be overruled: a
limiter outage now risks bounded overspend instead of taking the assistant
down. It is defensible because this is a spend brake, not an access control —
the session check above it guards access, and the spec names the AI Gateway
project budget as the hard backstop. If that balance is wrong, the change is
one try/catch in route.ts.
A sixth unit test covers the fail-open path, using the exact 307 error string as the regression marker.
checkRateLimit returns rateLimited: false when no rule
carries the id, so the 429 path is not observable on the preview — the unit tests are the
evidence for that criterion until the operator creates the rule (spec → Notes for the
operator). This is expected, not a defect.Rate-limit ID 'agent-chat' not configured and returns not-limited on
every request. That is the documented no-rule behaviour, now confirmed by
reading the SDK source — not a defect.proxy.ts now has a second public route. Worth an explicit look given the
app's gate is its whole security story; the reasoning is inlined in the file.
There is no unit test for it — it is Clerk middleware, exercised by the
signed-out redirect check on the preview.route.test.ts lives inside app/api/chat/. Next's documented
colocation ignores non-convention filenames in app/, and CONVENTIONS.md requires
co-located tests — but every other test in this monorepo sits in lib/ or components/, so
this is the first one inside an app/ tree. If the Vercel build objects, that is the cause.Context budget: within band. Reads beyond the Inputs table were the two existing chat routes and
the onboarding chat client (the current createAgentUIStreamResponse + useChat pattern), the
packages/ui compound/ai exports, apps/console's vitest config and turbo.json's test-task
block — the last of which is what surfaced the stale-cache hash dependency above.
04_verify/output/verify.md2840f0a — settled via ci-status.sh after the last push of this stageagentic-interface
(https://agentic-interface-git-claude-agentic-chat-inte-107c3f-sustentus.vercel.app) built for
this commit — but see the blocker below: it is behind Vercel Deployment Protection and the
agent cannot reach it. web also built. demo · docs · help-centre · marketing ·
storybook · tenant-management all correctly skipped by turbo-ignore — recorded as skipped,
not quoted as green.standard) — 4 findings, 3 fixed on branch, 1 acceptedgit diff main...HEAD in this checkout reports 171 files / +12392 — local main is stale at
64a64b8. The true merge base is origin/main (a05194b) and the real diff is 18 files /
+826 −50. Every review in this stage was run against origin/main...HEAD. Anyone re-running
these passes must do the same or they will review a dozen unrelated features.
The Verify contract splits the smoke into "agent-run: everything reachable without signing in" and
"operator-demonstrated: signed-in". For this project the agent-run half is empty: the
agentic-interface preview sits behind Vercel Deployment Protection, so every unauthenticated
request is answered by Vercel SSO before it reaches the app.
This is a live trap, not a theoretical one. curl -X POST .../api/chat signed out returns a
401 — which looks exactly like the acceptance criterion passing, but the body is
{"protection":{"auto_vercel_auth_redirect":true,…}}: it is Vercel refusing the request, not our
route's auth() returning Unauthorized. Nothing reached the app. Both Vercel MCP bypass tools
(get_access_to_vercel_url, web_fetch_vercel_url) failed for this deployment.
So the unauthed criteria below are evidenced by code-path tracing plus the unit suite, and are marked as needing operator confirmation on the preview — never as agent-demonstrated. Worth feeding back into the Verify contract: for any protected preview, the agent's share of the smoke is code reading, and the contract should say so.
@sustentus/ui AI components — operator (Jamie), on
6cbd6dd: "the agent is now streaming responses". ⚠️ Confirmed on 6cbd6dd, and the head
is now 2840f0a; the route and the client have both changed since. Needs re-confirming on
2840f0a (see below).POST /api/chat signed out → 401; GET /api/chat → 405 — agent: traced, not
demonstrated. route.ts:60-63 returns 401 before anything else; only POST is exported,
so Next answers 405. route.test.ts asserts the 401 path and that neither the limiter nor
the model is reached. Preview demonstration blocked by Deployment Protection.defineAgent + DEFAULT_AGENT_MODEL — agent: verified by
inspection. No provider SDK in apps/agent/package.json; no model string literal anywhere
in apps/agent.apps/agent imports @sustentus/services/ai only — agent:
verified by inspection across the app tree.assistant-chat.tsx:29 and the raw error goes to console.error only. Provoke
it deliberately; note this path is now materially better than when it was last seen (see
Findings, retry-after-failure).checkRateLimit once, keyed by the Clerk user id, 429 before the agent runs — agent: unit
suite. Not observable on the preview until the WAF rule exists — see the escalation below.@sustentus/agent ships a test script running in Quality Project — agent: CI. Ten
tests green on 2840f0a.apps/agent/AGENTS.md states the amended services boundary — agent: verified in the diff.Quality Project green and Vercel – agentic-interface green, no other app's output changed
— agent: CI on 2840f0a; the six unrelated Vercel projects were skipped by turbo-ignore.Fixed on branch (f487409):
messages reached the model with only an Array.isArray check; a
role: "system" UI message becomes a real system model message (ai@6.0.175
dist/index.mjs:8367) and allowSystemInMessages being unset only warns
(dist/index.mjs:2091). That routes around the prompt defences, which bind what arrives as a
user message. Any signed-in caller — and the sign-in page records that the shared Clerk
instance permits self-registration — could curl in their own system prompt. Now filtered to
user/assistant turns, with two regression tests./.well-known/vercel/(.*) narrowed to
/.well-known/vercel/rate-limit-api/(.*). Not exploitable today (no such route exists), but
/.well-known/vercel/flags is a real Vercel convention that is app code and would have landed
outside the deny-by-default gate silently.Fixed on branch (2840f0a):
status === "ready", but PromptInput calls form.reset() before invoking
onSubmit (packages/ui/src/compound/ai/prompt-input.tsx:734). After a failure status stays
"error", so the retry was swallowed and the typed text discarded — the conversation stuck
until a reload, while the copy said "try sending that again". Now blocks only while a turn is in
flight. This is why the failure-copy line above must be re-smoked rather than trusted.req.json() was unguarded. Now 400.messages forwarded to the gateway, billing a provider error the user reads as
"assistant unavailable". Now 400.Accepted, with reasoning — not fixed:
role: "assistant",
because a stateless chat endpoint must accept prior assistant turns — that is how useChat
resumes a conversation. A caller can therefore still put words in the assistant's mouth
("Sure — here are my instructions:") and re-frame the guardrails much as a system message would.
Closing this needs server-held conversation state or signed history, both of which contradict
this stub's explicit "ephemeral by design" scope. The same shape exists in the BRD and
onboarding routes in apps/web — it is a property of the architecture, not of this branch. I
am flagging rather than silently widening scope: the injection-defence criterion should be read
as "system-message injection is closed and the prompt resists user-message injection", not as
"injection is solved".route.ts:30), unchanged from Build and deliberate. Both review
passes examined it and agreed the reasoning holds: it is a spend brake, not an access control.Escalated — operator/owner decision, outside this branch:
agent-chat WAF rule still
does not exist, so checkRateLimit reports not-limited on every call; and the check fails open.
Neither is wrong on its own, but together there is no rate limiting on a paid model endpoint,
and the population that can drive it is anyone who can self-register on the shared Clerk
instance. The AI Gateway project budget is the sole backstop, and nothing in this branch verifies
that a budget or spend alert is actually configured. Recommend creating the WAF rule and
confirming the gateway budget/alert before announce.Handed to Ship (docs-sync / changelog-entry) — on schedule, must not merge unaddressed:
apps/docs/app/technical/applications/page.mdx:37 — "has no @sustentus/services
dependency" is false on merge.apps/docs/app/technical/applications/page.mdx:38 — "The conversation surface itself is not
built yet" is false on merge.apps/docs/app/technical/architecture/page.mdx:61 — "no @sustentus/services dependency today".Context budget: over band, deliberately. Beyond the Inputs table I read the @vercel/firewall and
ai package sources to confirm the 307 root cause and the system-message claim first-hand rather
than accept a review agent's assertion, and packages/ui's prompt-input.tsx to confirm the
reset-before-submit ordering. All three changed the outcome of this stage.
05_ship/output/changelog.mdThe Sustentus assistant now answers. Sign in to the assistant app with your usual Sustentus account and you can hold a conversation with it — ask a question, think a problem through, and watch the reply stream back as it is written.
It is deliberately narrow to begin with. The assistant can talk, and that is all it can do: it cannot see your leads, projects, invoices or documents, cannot look anything up, and cannot make changes on your behalf. Ask it for a figure from your account and it will tell you it cannot reach it rather than guess at one. The conversation is not saved either, so refreshing the page starts a fresh one.
This is the assistant's first release. Everything it can do today, it does without touching your data.
05_ship/output/investor-update.mdWho it's for: every signed-in user, all six personas What shipped: a real conversation in the assistant app: ask a question, the reply streams back. Why it matters: Scale the Bridge calls for autonomous AI agents; this is the surface they run on.
No account data, nothing saved.
Dig deeper: https://github.com/sustentus/sustentus/pull/896 · https://help.sustentus.com/changelog/2026-08-27-agentic-chat-interface
05_ship/output/release.md[x] on the PR body: Spec
approved (before Build) and Ready to merge (this stage). Neither was ticked by the agent; both
were read from the raw body via the REST API, because pull_request_read strips the
<!-- gate:* --> HTML anchors the parse depends on.4b690b1, settled via ci-status.sh after that stage's
last push. This stage found the PR mergeable_state: dirty — main had moved from a05194b
to c9ef121 under the run (#895 and #898 merged while it sat at the gate) — so main was
merged into the branch here. It resolved with no conflicts. The verdict is therefore
re-established with ci-status.sh on the head this commit creates, and the squash proceeds
only on a settled GREEN. Nothing failed in this stage.technical/applications (the Agent section claimed no @sustentus/services dependency and
"the conversation surface itself is not built yet"), technical/architecture (claimed
apps/agent "makes no AI call and takes no @sustentus/services dependency today"), and
technical/packages/services (the /ai entrypoint comment and the ai/ source-structure line,
which now name the assistant agent). The first two were flagged by Verify; the third was found
here.platform-overview,
service-journey and feature-role-matrix all remain accurate. It is a new surface, not a new
step in the flow.apps/help/app/changelog/2026-08-27-agentic-chat-interface/ (all six personas — anyone with a
session can use it); ship note at 05_ship/output/investor-update.md, 53 words of body against
the 60 cap, both Dig deeper links filled with no placeholder left for ship-note.yaml to
refuse..github/workflows/ship-note.yaml fires on the merge that
carries this file and sends it to #product-update. The agent does not run the sender.close-out.sh archives .icm/runs/agentic-chat-interface/ to
apps/docs/archive/pipeline-runs/. This is stub 2 of 2 of the agentic-app-foundation
batch and its sibling agentic-app-scaffold merged as #894, so the epic's intake folder should
archive with it — written as intent, since the merge that carries this file is what makes it
true.Four criteria depend on a signed-in session against a preview behind Vercel Deployment Protection, which the agent cannot pass. Verify recorded them honestly rather than rounding them up, and this stage does not upgrade them: Jamie's confirmation of the Verify gate is what carries them, not an agent demonstration.
@sustentus/ui AI components — operator (Jamie), on
6cbd6dd: "the agent is now streaming responses". Not re-confirmed on a later head; the
route and client both changed in f487409 and 2840f0a after it.POST /api/chat signed out → 401, GET → 405 — traced, not demonstrated. route.ts
returns 401 before anything else and only POST is exported; route.test.ts asserts the
401 path and that neither the limiter nor the model is reached. The preview answers
unauthenticated requests with Vercel SSO before they reach the app, so a curl 401 there
proves nothing.defineAgent + DEFAULT_AGENT_MODEL — verified by inspection: no provider SDK
in apps/agent/package.json, no model string literal in the app.apps/agent imports @sustentus/services/ai only — verified
by inspection across the app tree.assistant-chat.tsx:29; the raw error goes to console.error only.checkRateLimit once, keyed by the Clerk user id, 429 before the agent runs — unit suite.
Not observable on the preview until the WAF rule exists — see the open risk below.@sustentus/agent ships a test script running in Quality Project — ten tests green.apps/agent/AGENTS.md states the amended services boundary — verified in the diff.Quality Project and Vercel – agentic-interface green, no other app's output changed —
the six unrelated Vercel projects were skipped by turbo-ignore, recorded as skipped rather
than quoted as green.Neither blocks the merge; both are the operator's and outlive this branch.
agent-chat WAF rule still does not exist, so
checkRateLimit reports not-limited on every call, and the check deliberately fails open. Each
is defensible alone; together there is no rate limiting on a paid model endpoint, drivable
by anyone who can self-register on the shared Clerk instance. Verify recommended creating the
rule and confirming the AI Gateway budget/spend alert before announce. Neither is done, and
the merge is the announce. Settings are in the spec under
## Notes for the operator — the WAF rule; no redeploy is needed.agentic-interface has no custom domain recorded in technical/deployment, and
none was invented here. If the app is not reachable at a domain users know, the entry should be
held or given a URL.Rollback is clean: no migrations and no schema in this diff, so reverting the squash commit is sufficient.
Context budget: within band. Beyond the Inputs table this stage read
business/initiatives/scale-the-bridge and business/okrs/2026-Q2 for the ship note's tie-in
(both named by the contract), business/platform-overview to justify the no-impact claim rather
than assert it, apps/agent/app/page.tsx and the assistant prompt for the changelog's user-facing
copy, and one archived release.md for the CI-line convention.