BuilderIO / BuilderIO/agent-native
Fresh database: workspace apps register with no org and then deny every member including the owner
- Dominant language
- TypeScript
- Stars
- 4.8k
- Forks
- 449
- Avg merge
- 10h 24m
- Merged PRs (30d)
- 883
Description
# Workspace apps self-register with no org on a fresh database, then deny every member
**Version:** `@agent-native/core` 0.178.1, `@agent-native/dispatch` 0.36.1, a
four-app workspace (chat, documents, brand-assets, dispatch) on Vercel with a
brand new empty Neon Postgres database.
**What happens.** On the first deploy against an empty database, the spoke apps
register themselves in `workspace_apps` before any organization exists. The rows
land with `org_id` NULL and `owner_email` empty:
```
id | org_id | owner_email | visibility
brand-assets | (null) | | org
chat | (null) | | org
documents | (null) | | org
```
The first user then signs in, and the framework creates their personal
organization, as documented. From that moment every authenticated request into
Chat, Documents and Assets answers **403**: `application-state`, `poll`,
`agent-chat/threads`, `agent-engine/status`, and every action. Dispatch keeps
working, because it takes a different branch. The workspace is unusable and the
owner has no way to see why; the UI reports it as "Could not load your setup."
**Why.** `dist/org/workspace-app-access.js`, `isWorkspaceAppAccessAllowed`:
```js
const resourceOrgId = (typeof app.org_id === "string" ? app.org_id : "").trim() || null;
const orgId = context.orgId?.trim() || null;
const sameOrg = !!resourceOrgId && resourceOrgId === orgId;
if (ownerEmail === email && (!resourceOrgId || sameOrg)) return true;
if (!sameOrg || !orgId) return false;
```
With `org_id` NULL, `sameOrg` is false. With `owner_email` empty, the owner
branch above it cannot match either. The function returns false for every
caller, including the organization owner, before it ever reaches the
`visibility === "org"` check below.
**The contradiction.** Migration `1018`,
`workspace-apps-restore-ownerless-legacy-visibility`, exists precisely to make
this population reachable:
```sql
UPDATE workspace_apps SET visibility = 'org'
WHERE visibility = 'private' AND TRIM(owner_email) = ''
AND NOT EXISTS (SELECT 1 FROM workspace_app_shares WHERE resource_id = workspace_apps.id)
```
So the framework deliberately marks ownerless apps as organization-visible, and
the access check then refuses them for want of an org id. The two rules disagree
about the same rows.
**Expected.** An ownerless, org-visible app should be reachable by members of
the deployment's organization. Either the access check treats a NULL
`resource_org_id` on an `org`-visible row as "belongs to the caller's org"
(the same latitude the owner branch already grants with `!resourceOrgId`), or
registration adopts the organization the first time one exists, or Dispatch
exposes a way to claim ownerless apps. Today none of the three happens.
**Impact.** Any new deployment on an empty database locks its own first user out
of every app except Dispatch, immediately after the documented first-run step of
signing in as the organization owner. It is not recoverable from the UI.
**Workaround.** A direct write, which is the only route we found:
```sql
UPDATE workspace_apps SET org_id = '' WHERE org_id IS NULL;
```
Three rows, and access is restored on the next request with no redeploy.
**Note on the existing first-run guidance.** Our own working agreement carries
the line "on a fresh database, sign in at `/dispatch` as the organization owner
before opening the other apps". That was followed here and it does not help:
the rows are written when the apps first boot, which precedes any sign-in.
Contributor guide
No contributing guide indexed for this repository
Research direction
Start with dist/org/workspace-app-access.js and the isWorkspaceAppAccessAllowed function, then read migration 1018, workspace-apps-restore-ownerless-legacy-visibility. Trace how fresh-deployment registration creates ownerless rows before sign-in. Done means an org-visible ownerless app is reachable by members of the deployment's organization without the direct SQL workaround.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- postgresql, typescript
- Domain
- authorization, backend, databases
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 52/100