Found while delivering #27 on claude/issue-27-signable-personas. The PR ships a workaround; the defect is upstream (@objectstack/plugin-security 17.3.0) and should be relayed to objectstack-ai/objectstack — filed here the way #18 was filed for objectstack#16518, because I could not verify write access to the upstream tracker from this seat.
The rule as implemented
Under the single tenancy posture the security bootstrap's seedDefaultPermissionSets still runs the first-user carve-out (plugin-security dist/index.js, the block ending in first user promoted to platform admin):
find('sys_user', {}, 50) — an unordered window of at most 50 users;
- filter out system rows, sort by
created_at ascending;
- the first user in that window that has a
sys_account row receives the legacy unscoped admin_full_access grant, and claimSeedOwnership hands it ownership of every seeded row.
Until #27, no seeded user had a sys_account, so step 3 found nobody ("human user rows exist but none can authenticate — platform admin NOT promoted"). The moment a seed provisions credentials, whoever the driver returns first in that window wins.
Measured
Same artifact, OS_PLATFORM_OWNER_EMAIL=admin@objectos.ai, seven credentialed users, the platform owner inserted first with id usr_ats_platform_owner:
| driver |
window order |
promoted |
| memory |
insertion order |
admin@objectos.ai (owner) |
| sqlite (default) |
by id — usr_ats_c01 … usr_ats_c50, owner not in the window |
candidate01@mail.example — the job-seeker persona |
Log line on sqlite, verbatim: [security] first user promoted to platform admin: candidate01@mail.example. That seeker then also owned every seeded row through claimSeedOwnership.
Workaround shipped in #27's PR
The owner row is pinned so that it wins under every ordering at once: id usr_ats_0_owner (collates before every other user id), inserted first, created_at a year older than every other user. Re-measured on both drivers: the owner is promoted on both. This is a seed-side pin of a rule the seed should not have to know about, and it holds only as long as no other seeded user sorts first on all three axes.
What upstream should probably do (one of)
- Prefer the configured
OS_PLATFORM_OWNER_EMAIL address when it is set and registered, instead of promoting anyone else — the config anchor already exists for the walled posture.
- Order the query by
created_at server-side across all users rather than sorting a 50-row window client-side.
- Skip the legacy promotion entirely when a configured owner has standing.
Filed without a label for triage; no Blocked-by: — the workaround lands with #27.
Found while delivering #27 on
claude/issue-27-signable-personas. The PR ships a workaround; the defect is upstream (@objectstack/plugin-security17.3.0) and should be relayed to objectstack-ai/objectstack — filed here the way #18 was filed for objectstack#16518, because I could not verify write access to the upstream tracker from this seat.The rule as implemented
Under the
singletenancy posture the security bootstrap'sseedDefaultPermissionSetsstill runs the first-user carve-out (plugin-securitydist/index.js, the block ending infirst user promoted to platform admin):find('sys_user', {}, 50)— an unordered window of at most 50 users;created_atascending;sys_accountrow receives the legacy unscopedadmin_full_accessgrant, andclaimSeedOwnershiphands it ownership of every seeded row.Until #27, no seeded user had a
sys_account, so step 3 found nobody ("human user rows exist but none can authenticate — platform admin NOT promoted"). The moment a seed provisions credentials, whoever the driver returns first in that window wins.Measured
Same artifact,
OS_PLATFORM_OWNER_EMAIL=admin@objectos.ai, seven credentialed users, the platform owner inserted first with idusr_ats_platform_owner:admin@objectos.ai(owner)usr_ats_c01 … usr_ats_c50, owner not in the windowcandidate01@mail.example— the job-seeker personaLog line on sqlite, verbatim:
[security] first user promoted to platform admin: candidate01@mail.example. That seeker then also owned every seeded row throughclaimSeedOwnership.Workaround shipped in #27's PR
The owner row is pinned so that it wins under every ordering at once: id
usr_ats_0_owner(collates before every other user id), inserted first,created_ata year older than every other user. Re-measured on both drivers: the owner is promoted on both. This is a seed-side pin of a rule the seed should not have to know about, and it holds only as long as no other seeded user sorts first on all three axes.What upstream should probably do (one of)
OS_PLATFORM_OWNER_EMAILaddress when it is set and registered, instead of promoting anyone else — the config anchor already exists for the walled posture.created_atserver-side across all users rather than sorting a 50-row window client-side.Filed without a label for triage; no
Blocked-by:— the workaround lands with #27.