Filed by the domain:ui execution seat (seat post #5560) as the ruled prerequisite for #5702. Filed unassigned and unlabelled: the file surface is build/dev configuration, and routing is triage's single-producer job — see "Routing clue" below. Sequencing matters here (the ruling names it), so this is not an ordinary queue position.
Why this card exists
Maintainer, 2026-08-23, on #5702: option A, sequenced — land sameOriginOnly as the action-runtime default, sequenced with a config repair card that empties VITE_SERVER_URL in the dev envs and revisits console-starter/.env.production. This is that card. #5702's own branch is written and pushed; it waits on this one.
The reason is measured, not anticipated. packages/app-shell/src/hooks/useConsoleActionRuntime.tsx prefixes any non-http action target with the configured server origin:
:326 const baseUrl = import.meta.env.VITE_SERVER_URL || '';
:346 const url = resolvedTarget.startsWith('http') ? resolvedTarget : `${baseUrl}${resolvedTarget}`;
and the committed environment files point that origin somewhere the page is not served from:
| file | committed value | page origin | verdict |
|---|
apps/console/.env.development:4 | http://localhost:3000 | :5180 (apps/console/vite.config.tsserver.port) | cross-origin |
examples/console-starter/.env.development:4 | http://localhost:3000 | :5173 (no server block ⇒ Vite default) | cross-origin |
examples/console-starter/.env.production:3 | https://demo.objectstack.ai | deployment origin | cross-origin unless deployed there |
apps/console/.env.production:7 | (empty) | same-origin | fine, do not touch |
Under #5702's ruled default, every relative-targettype: 'api' action in those configurations resolves cross-origin and is fetched bare — no Authorization, no X-Tenant-ID, no Accept-Language — i.e. 401 for the standard pnpm dev stack. That is not an exotic deployment; it is what a contributor gets out of the box.
⚠️The blast radius is wider than one handler.sameOriginOnly would be added at useConsoleActionRuntime.tsx:308, and that authFetch is consumed at :399 (apiHandler), :534, :592, :695, and is returned from the hook at :721 — so external consumers get it too. Four of those lanes build URLs from VITE_SERVER_URL (:326, :509, :593, :696). Any repair scoped to apiHandler alone misses the rest.
What this card should establish, then do
The ruling's own phrase for the target state is "proxies same-origin". Concretely:
apps/console — the proxy already exists: vite.config.ts has proxy: { '/api': { target: process.env.DEV_PROXY_TARGET || 'http://localhost:3000', changeOrigin: true } }. So emptying VITE_SERVER_URL in .env.development should route /api/... same-origin through it. Verify that end to end rather than assuming — including any lane that builds a URL from VITE_SERVER_URL for a path that is not under /api, which the proxy would not cover.examples/console-starter — vite.config.ts has no server block at all, so there is no proxy and no pinned port. Adding the same one-stanza proxy (and emptying .env.development) is the parallel repair.examples/console-starter/.env.production — revisit per the ruling. A committed production origin of https://demo.objectstack.ai is a different question from the dev stack and may be deliberate; establish which before changing it.
⛔ Do not change apps/console/.env.production — its VITE_SERVER_URL is already empty (same-origin) and is the shape the others are being moved toward.
⛔ Do not implement #5702's sameOriginOnly change here. That card is written, pushed and waiting; this card is only the ground it lands on.
Establish-first, before any edit
- Is
VITE_SERVER_URL read anywhere that would break if it were empty? There are ~20 read sites across packages/app-shell/** alone (ConditionalAuthWrapper.tsx:52, AiChatPage.tsx:374, marketplaceApi.ts:19, useAiSurface.ts:55, sharedUserFeeds.ts:524, AppHeader.tsx:303, …). Most coalesce to '', which is the same-origin shape — verify, do not assume, and name any that do not. - Does every path those sites build actually sit under
/api, i.e. inside what the proxy covers? A read that builds ${VITE_SERVER_URL}/something-else is the failure mode that turns "empty it" from a fix into an outage. - ⛔ Any zero result in the above needs a control term known to hit, or it is not a reading.
Landing order (the ruled part)
This card lands first, or in the same stroke as #5702. Landing #5702 alone breaks the dev stack; landing this alone is harmless (it converges the configs on one origin regardless). If the two cannot be sequenced, say so rather than landing #5702 first.
Routing clue (label deliberately not applied here)
The surface is .env.* files and a vite.config.tsserver block — build/dev tooling, which reads as domain:devx. But the blocking relationship and the blast-radius reasoning live entirely in domain:ui's action runtime, and the domain:ui seat is holding #5702 against it. Triage's call; this seat did not label it.
Refs
Filed by the
domain:uiexecution seat (seat post #5560) as the ruled prerequisite for #5702. Filed unassigned and unlabelled: the file surface is build/dev configuration, and routing is triage's single-producer job — see "Routing clue" below. Sequencing matters here (the ruling names it), so this is not an ordinary queue position.Why this card exists
Maintainer, 2026-08-23, on #5702: option A, sequenced — land
sameOriginOnlyas the action-runtime default, sequenced with a config repair card that emptiesVITE_SERVER_URLin the dev envs and revisitsconsole-starter/.env.production. This is that card. #5702's own branch is written and pushed; it waits on this one.The reason is measured, not anticipated.
packages/app-shell/src/hooks/useConsoleActionRuntime.tsxprefixes any non-httpaction target with the configured server origin:and the committed environment files point that origin somewhere the page is not served from:
apps/console/.env.development:4http://localhost:3000:5180(apps/console/vite.config.tsserver.port)examples/console-starter/.env.development:4http://localhost:3000:5173(noserverblock ⇒ Vite default)examples/console-starter/.env.production:3https://demo.objectstack.aiapps/console/.env.production:7Under #5702's ruled default, every relative-target
type: 'api'action in those configurations resolves cross-origin and is fetched bare — noAuthorization, noX-Tenant-ID, noAccept-Language— i.e. 401 for the standardpnpm devstack. That is not an exotic deployment; it is what a contributor gets out of the box.sameOriginOnlywould be added atuseConsoleActionRuntime.tsx:308, and thatauthFetchis consumed at:399(apiHandler),:534,:592,:695, and is returned from the hook at:721— so external consumers get it too. Four of those lanes build URLs fromVITE_SERVER_URL(:326,:509,:593,:696). Any repair scoped toapiHandleralone misses the rest.What this card should establish, then do
The ruling's own phrase for the target state is "proxies same-origin". Concretely:
apps/console— the proxy already exists:vite.config.tshasproxy: { '/api': { target: process.env.DEV_PROXY_TARGET || 'http://localhost:3000', changeOrigin: true } }. So emptyingVITE_SERVER_URLin.env.developmentshould route/api/...same-origin through it. Verify that end to end rather than assuming — including any lane that builds a URL fromVITE_SERVER_URLfor a path that is not under/api, which the proxy would not cover.examples/console-starter—vite.config.tshas noserverblock at all, so there is no proxy and no pinned port. Adding the same one-stanza proxy (and emptying.env.development) is the parallel repair.examples/console-starter/.env.production— revisit per the ruling. A committed production origin ofhttps://demo.objectstack.aiis a different question from the dev stack and may be deliberate; establish which before changing it.⛔ Do not change
apps/console/.env.production— itsVITE_SERVER_URLis already empty (same-origin) and is the shape the others are being moved toward.⛔ Do not implement #5702's
sameOriginOnlychange here. That card is written, pushed and waiting; this card is only the ground it lands on.Establish-first, before any edit
VITE_SERVER_URLread anywhere that would break if it were empty? There are ~20 read sites acrosspackages/app-shell/**alone (ConditionalAuthWrapper.tsx:52,AiChatPage.tsx:374,marketplaceApi.ts:19,useAiSurface.ts:55,sharedUserFeeds.ts:524,AppHeader.tsx:303, …). Most coalesce to'', which is the same-origin shape — verify, do not assume, and name any that do not./api, i.e. inside what the proxy covers? A read that builds${VITE_SERVER_URL}/something-elseis the failure mode that turns "empty it" from a fix into an outage.Landing order (the ruled part)
This card lands first, or in the same stroke as #5702. Landing #5702 alone breaks the dev stack; landing this alone is harmless (it converges the configs on one origin regardless). If the two cannot be sequenced, say so rather than landing #5702 first.
Routing clue (label deliberately not applied here)
The surface is
.env.*files and avite.config.tsserverblock — build/dev tooling, which reads asdomain:devx. But the blocking relationship and the blast-radius reasoning live entirely indomain:ui's action runtime, and thedomain:uiseat is holding #5702 against it. Triage's call; this seat did not label it.Refs
type: 'api'actions send the Bearer token and X-Tenant-ID to absolute third-party URLs — the #2725sameOriginOnlymitigation was never applied to this lane #5702 — the ruled card this unblocks (branchclaude/issue-5702-action-runtime-same-origin, head074121474, pushed and unlanded)X-Tenant-IDgating question the ruling defers to a follow-upsameOriginOnlymitigation, applied then toConsoleShell.tsx:213only