Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,8 +118,12 @@ permission set (DESIGN.md §04).
Each one is measured, with the measurement in the issue. None is hidden by the demo.

- [#39](https://github.com/objectstack-ai/ats/issues/39) — on the default (sqlite) driver the four
tenancy-scoped objects return no rows to the platform personas; run the demo on
`--database-driver memory` (upstream [objectstack#16589](https://github.com/objectstack-ai/objectstack/issues/16589)).
tenancy-scoped objects (`ats_employer`, `ats_employer_member`, `ats_interview`, `ats_offer`) return
no rows to a session whose active organization is the Default Organization. Since the app declares
`membershipPolicy: 'invite-only'` that is the platform **owner** (`admin@objectos.ai`) alone; the
`admin@platform.example` and `ops@platform.example` personas hold no membership and read all four on
either driver. Sign in as those two, or run the demo on `--database-driver memory`
(upstream [objectstack#16589](https://github.com/objectstack-ai/objectstack/issues/16589)).
- [#45](https://github.com/objectstack-ai/ats/issues/45) — an employer administrator cannot yet
*create* a job (blocked upstream on
[objectstack#16607](https://github.com/objectstack-ai/objectstack/issues/16607) and
Expand Down
95 changes: 95 additions & 0 deletions docs/evidence/issue-38/01-backfill-and-personas-memory.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
# Membership backfill before / after — driver: memory, port 4621, ats @ cdbadc4 (#38)
# BEFORE = the same worktree with AtsAuthMembershipPolicyPlugin removed from objectstack.config.ts
# plugins: [] (ablation, restored byte-identically afterwards); AFTER = as committed.

## BEFORE — boot log, the backfill line
2026-09-07T19:49:29.397Z INFO [auth] membership backfill (app:seeded) bound 82 member-less user(s) to the default organization (ADR-0093 D6) {"scanned":113,"bound":82,"skipped":31}

## AFTER — boot log, every line mentioning the policy or the backfill
2026-09-07T19:50:24.397Z INFO [ats] auth membership policy declared 'invite-only' (was 'auto') in init() — DESIGN.md §03: sign-up and the ADR-0093 D6 backfill bind nobody to the default organization. An explicit auth.membership_policy setting or OS_AUTH_MEMBERSHIP_POLICY still overrides this at kernel:ready.
(the ADR-0093 D6 backfill logs only when bound > 0 — plugin-auth/src/auth-plugin.ts;
the absence of the line IS the 0-bound reading, and the ablation above is its control)

## BEFORE — read as the platform owner
## sign-in owner: HTTP 200
sys_member: total=113 hasMore=false len=113 consistent=true
sys_member per organization:
org_mtrnmk8eefixzedy 83
org_ats_quillstone 3
org_ats_harborline 3
org_ats_cedarbrook 3
org_ats_brightmarket 3
org_ats_lumenvale 3
org_ats_summitridge 3
org_ats_swiftroute 2
org_ats_bluewater 2
org_ats_ironbridge 2
org_ats_pixelforge 2
org_ats_meridian 2
org_ats_orbit 2
sys_organization: total=13 hasMore=false len=13 consistent=true
organizations: org_ats_quillstone(Quillstone Robotics), org_ats_harborline(Harborline Manufacturing), org_ats_cedarbrook(Cedarbrook Care Network), org_ats_brightmarket(Brightmarket Retail Group), org_ats_lumenvale(Lumenvale Academy), org_ats_summitridge(Summitridge Financial), org_ats_swiftroute(Swiftroute Logistics), org_ats_bluewater(Bluewater Hospitality Group), org_ats_ironbridge(Ironbridge Construction), org_ats_pixelforge(Pixelforge Studios), org_ats_meridian(Meridian Clinics), org_ats_orbit(Orbit Consulting Partners), org_mtrnmk8eefixzedy(Default Organization)
sys_user: total=113 hasMore=false len=113 consistent=true

## per-persona reads (total / hasMore / len)
persona employer employer_member interview offer job application candidate inquiry
owner 12 30 40 23 40 200 80 8
activeOrganizationId="org_mtrnmk8eefixzedy" userId=usr_ats_0_owner
platform-admin 12 30 40 23 40 200 80 8
activeOrganizationId="org_mtrnmk8eefixzedy" userId=usr_ats_platform_admin
platform-ops 12 30 40 23 40 200 80 8
activeOrganizationId="org_mtrnmk8eefixzedy" userId=usr_ats_platform_ops
quillstone-admin 1 3 10 3 5 27 70 2
activeOrganizationId="org_ats_quillstone" userId=usr_ats_quillstone_admin
quillstone-rec 1 3 10 3 5 27 70 2
activeOrganizationId="org_ats_quillstone" userId=usr_ats_quillstone_r1
harborline-admin 1 3 6 4 5 31 69 2
activeOrganizationId="org_ats_harborline" userId=usr_ats_harborline_admin
seeker-01 9 ERR 0 1 22 1 1 ERR
activeOrganizationId="org_mtrnmk8eefixzedy" userId=usr_ats_c01
! ats_employer_member: HTTP 403 {"error":"You do not have permission to perform this action. Contact your administrator if you need access.","code":"PERMISSION_DENIED","object":"ats_employer_member"}
! ats_inquiry: HTTP 403 {"error":"You do not have permission to perform this action. Contact your administrator if you need access.","code":"PERMISSION_DENIED","object":"ats_inquiry"}

# end

## AFTER — read as the platform owner
## sign-in owner: HTTP 200
sys_member: total=31 hasMore=false len=31 consistent=true
sys_member per organization:
org_ats_quillstone 3
org_ats_harborline 3
org_ats_cedarbrook 3
org_ats_brightmarket 3
org_ats_lumenvale 3
org_ats_summitridge 3
org_ats_swiftroute 2
org_ats_bluewater 2
org_ats_ironbridge 2
org_ats_pixelforge 2
org_ats_meridian 2
org_ats_orbit 2
org_mtrnnrr350qhbok2 1
sys_organization: total=13 hasMore=false len=13 consistent=true
organizations: org_ats_quillstone(Quillstone Robotics), org_ats_harborline(Harborline Manufacturing), org_ats_cedarbrook(Cedarbrook Care Network), org_ats_brightmarket(Brightmarket Retail Group), org_ats_lumenvale(Lumenvale Academy), org_ats_summitridge(Summitridge Financial), org_ats_swiftroute(Swiftroute Logistics), org_ats_bluewater(Bluewater Hospitality Group), org_ats_ironbridge(Ironbridge Construction), org_ats_pixelforge(Pixelforge Studios), org_ats_meridian(Meridian Clinics), org_ats_orbit(Orbit Consulting Partners), org_mtrnnrr350qhbok2(Default Organization)
sys_user: total=113 hasMore=false len=113 consistent=true

## per-persona reads (total / hasMore / len)
persona employer employer_member interview offer job application candidate inquiry
owner 12 30 40 23 40 200 80 8
activeOrganizationId="org_mtrnnrr350qhbok2" userId=usr_ats_0_owner
platform-admin 12 30 40 23 40 200 80 8
activeOrganizationId=null userId=usr_ats_platform_admin
platform-ops 12 30 40 23 40 200 80 8
activeOrganizationId=null userId=usr_ats_platform_ops
quillstone-admin 1 3 10 3 5 27 70 2
activeOrganizationId="org_ats_quillstone" userId=usr_ats_quillstone_admin
quillstone-rec 1 3 10 3 5 27 70 2
activeOrganizationId="org_ats_quillstone" userId=usr_ats_quillstone_r1
harborline-admin 1 3 6 4 5 31 69 2
activeOrganizationId="org_ats_harborline" userId=usr_ats_harborline_admin
seeker-01 9 ERR 0 1 22 1 1 ERR
activeOrganizationId=null userId=usr_ats_c01
! ats_employer_member: HTTP 403 {"error":"You do not have permission to perform this action. Contact your administrator if you need access.","code":"PERMISSION_DENIED","object":"ats_employer_member"}
! ats_inquiry: HTTP 403 {"error":"You do not have permission to perform this action. Contact your administrator if you need access.","code":"PERMISSION_DENIED","object":"ats_inquiry"}

# end
89 changes: 89 additions & 0 deletions docs/evidence/issue-38/02-backfill-and-personas-sqlite.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# Membership backfill before / after — driver: sqlite (persistent file), port 4621, ats @ cdbadc4 (#38)
# The REST sys_member census is itself organization-filtered on sqlite, so the authoritative
# counts below are read straight out of the database file with better-sqlite3.

## BEFORE — boot log, the backfill line
2026-09-07T19:49:52.246Z INFO [auth] membership backfill (app:seeded) bound 82 member-less user(s) to the default organization (ADR-0093 D6) {"scanned":113,"bound":82,"skipped":31}
## BEFORE — sys_member, read directly from the sqlite file
# direct sqlite read — /tmp/claude-0/-home-user/b5e239b4-1c2d-5c61-92c7-2d1a1706b178/scratchpad/issue-38/b4.db
sys_member total: 113
sys_user total: 113
per organization:
org_mtrnn1kpyt1xfpth 83
org_ats_brightmarket 3
org_ats_cedarbrook 3
org_ats_harborline 3
org_ats_lumenvale 3
org_ats_quillstone 3
org_ats_summitridge 3
org_ats_bluewater 2
org_ats_ironbridge 2
org_ats_meridian 2
org_ats_orbit 2
org_ats_pixelforge 2
org_ats_swiftroute 2

## AFTER — boot log, every line mentioning the policy or the backfill
2026-09-07T19:50:46.199Z INFO [ats] auth membership policy declared 'invite-only' (was 'auto') in init() — DESIGN.md §03: sign-up and the ADR-0093 D6 backfill bind nobody to the default organization. An explicit auth.membership_policy setting or OS_AUTH_MEMBERSHIP_POLICY still overrides this at kernel:ready.
## AFTER — sys_member, read directly from the sqlite file
# direct sqlite read — /tmp/claude-0/-home-user/b5e239b4-1c2d-5c61-92c7-2d1a1706b178/scratchpad/issue-38/a4.db
sys_member total: 31
sys_user total: 113
per organization:
org_ats_brightmarket 3
org_ats_cedarbrook 3
org_ats_harborline 3
org_ats_lumenvale 3
org_ats_quillstone 3
org_ats_summitridge 3
org_ats_bluewater 2
org_ats_ironbridge 2
org_ats_meridian 2
org_ats_orbit 2
org_ats_pixelforge 2
org_ats_swiftroute 2
org_mtrno906rwpuxijt 1

## BEFORE — per-persona reads
## per-persona reads (total / hasMore / len)
persona employer employer_member interview offer job application candidate inquiry
owner 0 0 0 0 40 200 80 8
activeOrganizationId="org_mtrnn1kpyt1xfpth" userId=usr_ats_0_owner
platform-admin 0 0 0 0 40 200 80 8
activeOrganizationId="org_mtrnn1kpyt1xfpth" userId=usr_ats_platform_admin
platform-ops 0 0 0 0 40 200 80 8
activeOrganizationId="org_mtrnn1kpyt1xfpth" userId=usr_ats_platform_ops
quillstone-admin 1 3 10 3 5 27 70 2
activeOrganizationId="org_ats_quillstone" userId=usr_ats_quillstone_admin
quillstone-rec 1 3 10 3 5 27 70 2
activeOrganizationId="org_ats_quillstone" userId=usr_ats_quillstone_r1
harborline-admin 1 3 6 4 5 31 69 2
activeOrganizationId="org_ats_harborline" userId=usr_ats_harborline_admin
seeker-01 0 ERR 0 0 22 1 1 ERR
activeOrganizationId="org_mtrnn1kpyt1xfpth" userId=usr_ats_c01
! ats_employer_member: HTTP 403 {"error":"You do not have permission to perform this action. Contact your administrator if you need access.","code":"PERMISSION_DENIED","object":"ats_employer_member"}
! ats_inquiry: HTTP 403 {"error":"You do not have permission to perform this action. Contact your administrator if you need access.","code":"PERMISSION_DENIED","object":"ats_inquiry"}

# end

## AFTER — per-persona reads
## per-persona reads (total / hasMore / len)
persona employer employer_member interview offer job application candidate inquiry
owner 0 0 0 0 40 200 80 8
activeOrganizationId="org_mtrno906rwpuxijt" userId=usr_ats_0_owner
platform-admin 12 30 40 23 40 200 80 8
activeOrganizationId=null userId=usr_ats_platform_admin
platform-ops 12 30 40 23 40 200 80 8
activeOrganizationId=null userId=usr_ats_platform_ops
quillstone-admin 1 3 10 3 5 27 70 2
activeOrganizationId="org_ats_quillstone" userId=usr_ats_quillstone_admin
quillstone-rec 1 3 10 3 5 27 70 2
activeOrganizationId="org_ats_quillstone" userId=usr_ats_quillstone_r1
harborline-admin 1 3 6 4 5 31 69 2
activeOrganizationId="org_ats_harborline" userId=usr_ats_harborline_admin
seeker-01 9 ERR 0 1 22 1 1 ERR
activeOrganizationId=null userId=usr_ats_c01
! ats_employer_member: HTTP 403 {"error":"You do not have permission to perform this action. Contact your administrator if you need access.","code":"PERMISSION_DENIED","object":"ats_employer_member"}
! ats_inquiry: HTTP 403 {"error":"You do not have permission to perform this action. Contact your administrator if you need access.","code":"PERMISSION_DENIED","object":"ats_inquiry"}

# end
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# Precedence and persistence — port 4621, ats @ cdbadc4 (#38)

## 1. An explicit OS_AUTH_MEMBERSHIP_POLICY overrides the app declaration (driver: memory, --fresh)
# boot env: OS_AUTH_MEMBERSHIP_POLICY=auto
2026-09-07T19:47:12.565Z INFO [ats] auth membership policy declared 'invite-only' (was 'auto') in init() — DESIGN.md §03: sign-up and the ADR-0093 D6 backfill bind nobody to the default organization. An explicit auth.membership_policy setting or OS_AUTH_MEMBERSHIP_POLICY still overrides this at kernel:ready.
2026-09-07T19:47:14.058Z INFO [auth] membership backfill (app:seeded) bound 82 member-less user(s) to the default organization (ADR-0093 D6) {"scanned":113,"bound":82,"skipped":31}
# The app patches in init(); AuthPlugin's settings binding runs at kernel:ready, AFTER it, and
# applies the env value because its source is not 'default' (isExplicit, auth-plugin.ts). The
# backfill then binds the 82 again — operator override > app declaration > platform default.
sys_member: total=113 hasMore=false len=113 consistent=true
sys_member per organization:
org_mtrnjnrybobmaknn 83
org_ats_quillstone 3
org_ats_harborline 3
org_ats_cedarbrook 3
org_ats_brightmarket 3
org_ats_lumenvale 3
org_ats_summitridge 3
org_ats_swiftroute 2
org_ats_bluewater 2
org_ats_ironbridge 2
org_ats_pixelforge 2
org_ats_meridian 2
org_ats_orbit 2

## 2. The declaration holds on a SECOND boot over a persistent sqlite file (no --fresh)
2026-09-07T19:46:42.011Z INFO [Seeder] Seed loading complete {"inserted":0,"updated":7,"skipped":811,"errored":0}
2026-09-07T19:46:40.811Z INFO [ats] auth membership policy declared 'invite-only' (was 'auto') in init() — DESIGN.md §03: sign-up and the ADR-0093 D6 backfill bind nobody to the default organization. An explicit auth.membership_policy setting or OS_AUTH_MEMBERSHIP_POLICY still overrides this at kernel:ready.
# sys_member read straight out of the file is unchanged across the two boots: 32 = 30 employer
# staff + the platform owner + the one user the admin create-user probe (section 3) created.
# direct sqlite read — /tmp/claude-0/-home-user/b5e239b4-1c2d-5c61-92c7-2d1a1706b178/scratchpad/issue-38/ats38.db
sys_member total: 32
sys_user total: 114
per organization:
org_ats_brightmarket 3
org_ats_cedarbrook 3
org_ats_harborline 3
org_ats_lumenvale 3
org_ats_quillstone 3
org_ats_summitridge 3
org_ats_bluewater 2
org_ats_ironbridge 2
org_ats_meridian 2
org_ats_orbit 2
org_ats_pixelforge 2
org_ats_swiftroute 2
org_mtrngrqg5miihcxu 2
setting tables: ["sys_setting","sys_setting_audit"]
sys_setting: 0 row(s); mentioning "membership": 0
sys_setting_audit: 0 row(s); mentioning "membership": 0

## 3. UPSTREAM: the admin create-user endpoint binds regardless of the policy
# plugin-auth/src/admin-user-endpoints.ts, bindUserToSoleOrganization() calls
# reconcileMembership(engine, userId, { policy: 'auto', ... }) with the policy HARD-CODED,
# so it never reads getMembershipPolicy(). Measured on the branch, sqlite, invite-only:
POST /api/v1/auth/admin/create-user (probe.1788810309814@mail.example) → HTTP 200 {"success":true,"data":{"user":{"id":"raaDvTKgNbjzngvweCrTt1RuC0u6prST","email":"probe.1788810309814@mail.example","name":"Membership Probe"},"placeholderEmail":false,"mustChangePassword":true,"organizationId":"org_mtrngrqg5miihcxu","membershipCreated":true}}
sys_member AFTER create-user: total=2 hasMore=false len=2 consistent=true
delta: 1
new user session activeOrganizationId="org_mtrngrqg5miihcxu"
# Reported, not fixed here: it is upstream, and it is arguably right (an operator creating an
# account IS an invitation) — but it is not what ADR-0093 D1's 'the ONE source both membership
# paths read' says, and on this app it puts a Setup-created user in the Default Organization
# even though the deployment says invite-only. The demo never calls it.
Loading
Loading