BuilderIO / BuilderIO/agent-native

Fresh database: workspace apps register with no org and then deny every member including the owner

Closed
#4,944 0 comments 0 reactions 0 assignees View on GitHub
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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.