Skip to content

ADR (mirror of cloud ADR-0024): Identity & Access Architecture — the open half that plugin-auth / platform-objects/identity implement, recorded in this repo with Provenance #14506

Description

@hotlong

Part of #14496 (maintainer ruling 2026-09-02, 「ok」 to option 2: mirror the open half, do not move files). Filed by the director seat, session session_01WXyGTWPbbreqXow7Z2pZCk. docs/adr/** is a governed surface: draft PR, human merge, no seat flips it ready. Clause-②: no (a record, not a contract change). domain:* is triage's.

What to write

A new local ADR under the next free number in docs/adr/ (133 records today; 0130 is the highest) that states, as this repo enforces them, the decisions of cloud docs/adr/0024-identity-and-access-architecture.md (Status: Accepted, founder, 2026-06-25) whose mechanism lives in open code. From the cloud record's own headings, the open half is:

  • D1 per-environment identity, never centralised (plugin-auth runs better-auth in the env);
  • D2 two user populations, two sources of truth;
  • D4 source-of-truth marking — managed vs env-native (sys_user / sys_account managedBy posture);
  • D5 identity from the IdP, authorization in the env; D5.2 the local user-management surface under SSO (split by population — the sys_user actions, break-glass password, the last-admin guard that cites "ADR-0024 D4/D5.2" today);
  • D6 SSO + SCIM per production env (the @better-auth/sso mechanism and sys_sso_provider incl. the opt-in domain verification cited as "ADR-0024 ②");
  • D7 portability / self-host preserved; D9 env users live in the env, org membership via better-auth.

Stays in cloud, cited as cloud ADR-0024: D3 cloud-as-IdP hub, D5.1 the cloud OP membership gate, D8 billing, D10 prod/dev metering and population lifecycle, the V1 roadmap and commercial framing.

Shape (copy docs/adr/0079-record-display-name.md)

  • A ## Provenance — read this before citing this file section first: this file records, in this repo, decisions taken in objectstack-ai/cloud ADR-0024 on 2026-06-25; the cloud record is the original and keeps the commercial half; this file is written from the cloud record and the code that implements it, with the implementing anchors named (packages/plugins/plugin-auth/src/**, packages/platform-objects/src/identity/sys-sso-provider.object.ts, sys-user.object.ts, last-admin-guard.ts).
  • Decisions restated in this repo's own words, each with the code anchor that enforces it; ⛔ no new decision, no widening — anything the cloud record did not decide is not decided here.
  • Register the file in docs/adr/PRIORITIZATION.md if that index is maintained by hand (check the file's own header); scripts/adr-anchors/ mapping for the files that will cite it is the re-pointing card's job ([finding] The SCIM/identity ADR-0071 citation resolves to the dataset semantic-layer record — 39 files point at a decision about multi-hop joins #14361), not this one's.

Follow-ups the accepting seat files (not this card)

Re-check

git -C ../cloud show origin/main:docs/adr/0024-identity-and-access-architecture.md | grep -n "^### D"
git grep -n -E "ADR-0024" origin/main -- packages/plugins/plugin-auth/src packages/platform-objects/src/identity | grep -v -i "cloud ADR-0024" | wc -l   # expect ~71 before #14361 lands

Refs: #14496 (the ruling) · #14361 (the re-pointing, blocked on this) · cloud ADR-0024 · ADR-0079 (the Provenance shape) · #14360 / #13816 (live identity work that cites these decisions).

Activity

  1. os-zhuang commented on Sep 4, 2026

    @os-zhuang
    Contributor

    分诊路由(本评论来自分诊座位)· R+150 — domain:services · priority:p2(保留 documentation + pm:queue)。

    判据实测于本卡自陈的实现锚:packages/plugins/plugin-auth/src/** 与 packages/platform-objects/src/identity/{sys-sso-provider,sys-user}.object.ts(git ls-files 实测三个 identity object 文件都在 packages/platform-objects/src/identity/ 下)。交付物是 docs/adr/** 的散文、不落在任何包里 ⇒ 锚定规则的「落点包」判据不适用,改用「谁能把它写对」:D1/D4/D5.2/D6 的机制半边由 plugin-auth 执行 ⇒ services 车道。

    三份镜像 ADR 同归一车道的完整理由、以及 p2 不提级为 p1 的判据,写在父单 #14496 的分诊评论 5545404025,⛔ 不在此重复。受管面 ⇒ draft PR + 人工合并(卡面已写明)。


    Generated by Claude Code

  2. os-warren commented on Sep 7, 2026

    @os-warren
    Collaborator

    Claim: domain:services PM seat · session session_01XpTx2tbq3pZRYAdoGt6E6Y · 2026-09-07

    分支 claude/issue-14506-adr-mirror-identity-access · worktree objectstack-issue-14506
    Clause-②: no(记录,不是契约变更 —— 与卡面自陈一致;diff 不会碰 packages/spec/**)

    ⛔ 受管面:交付物在 docs/adr/**。draft PR + 人工合并,⛔ 席位不 flip ready、⛔ PM 不 arm auto-merge。

    派发前 PM 实测(⛔ 不是按卡面复述)

    Refs: #14496(裁定)· #14361(改指,阻塞于本卡)· ADR-0079(Provenance 体例)


    Generated by Claude Code

  3. os-warren commented on Sep 7, 2026

    @os-warren
    Collaborator

    os-dev-report

    {
      "issue": 14506,
      "status": "ok",
      "status_in_base_vocabulary": "done",
      "branch": "claude/issue-14506-adr-mirror-identity-access",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/16478",
      "pr_number": 16478,
      "pr_is_draft": true,
      "head_sha": "e1e5d0baa46b3b8d8b122ef5f44e201b94449b31",
      "base_at_branch_cut": "5a9138703d7461d2f586486509b2035d15450c16",
      "adr_file": "docs/adr/0132-identity-and-access-architecture.md",
      "adr_number_collision_check": "none — 0132 was free on origin/main 5a9138703 (highest 0131); pnpm check:adr-anchors green (it enforces one decision per number)",
      "premise_still_valid": true,
      "summary": "Wrote docs/adr/0132-identity-and-access-architecture.md (272 lines, the only changed file): this repo's own record of the OPEN half of cloud ADR-0024 — D1 per-env identity, D2 two populations / two sources of truth, D4 managed-vs-env-native marking, D5 identity-from-IdP with authorization decided in the env, D5.2 the SSO-era local user-management surface, D6 per-env SSO incl. the opt-in domain verification cited as ADR-0024 ②, D7 portability/self-host, D9 env users + better-auth organization membership. Slug reuses the cloud original's slug, per ADR-0079's stated precedent. Provenance section first, per the card. The commercial half (D3, D5.1, D8, D10, V1) is carried as an explicit table of what STAYS in cloud, plus a 'What this record does NOT settle' section — no decision added, none widened. Every clause carries a symbol anchor (path#symbol, never a line number) that I opened and read; all resolve under check-adr-symbol-anchors. PRIORITIZATION.md deliberately NOT edited — see notes_on_card_and_brief. Draft PR opened; skip-changeset applied and read back. ⛔ Not flipped ready, ⛔ no auto-merge, ⛔ no update_pull_request_branch.",
      "measured_adr_0024_bare_citations": {
        "command": "git grep -n -E 'ADR-0024' -- packages/plugins/plugin-auth/src packages/platform-objects/src/identity | grep -v -i 'cloud ADR-0024' | wc -l",
        "at_5a9138703_this_branch_base": 64,
        "at_00ff228fe_the_commit_the_card_cites": 59,
        "unfiltered_at_5a9138703": 76,
        "unfiltered_at_00ff228fe": 71,
        "verdict": "The card's '~71' does NOT reproduce with the card's own command. 71 is the UNFILTERED count at 00ff228fe; the filtered command there yields 59, and on today's origin/main 64. Reported as measured, not smoothed. Matters to #14361, whose scope figure comes from the same reading.",
        "checkout_was_shallow": false
      },
      "cloud_recheck": "SKIPPED — NOT RUN. The first re-check on the card needs the cloud repo (git -C ../cloud show origin/main:docs/adr/0024-...). This seat has no cloud checkout and no cloud access. Nothing in the ADR is derived from the cloud file; the record's own Provenance section declares that limitation in the document, so a later reader is not misled about what was checked.",
      "governed_merge_readings": {
        "positive": {
          "command": "node scripts/pm/check-governed-merges.mjs --test docs/adr/0132-identity-and-access-architecture.md",
          "exit": 3,
          "verdict_line": "GOVERNED — a human merge is the review record for this PR (#9495 regime). docs/adr/** x1"
        },
        "negative_control": {
          "command": "node scripts/pm/check-governed-merges.mjs --test packages/plugins/plugin-auth/src/auth-manager.ts",
          "exit": 0,
          "verdict_line": "NOT governed — ordinary queue landing applies to a PR with exactly this file list."
        }
      },
      "gates": {
        "derivation": "node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands — 17 commands; re-derived after the final commit and byte-identical to the pre-commit derivation",
        "union_run_at_commit": "e1e5d0baa",
        "exit_code_capture": "each command redirected to its own file, EXIT captured on the next statement; ⛔ never through a pipe",
        "results": [
          { "cmd": "node scripts/check-adr-links.mjs", "exit": 0 },
          { "cmd": "node scripts/check-adr-links.mjs --self-test", "exit": 0 },
          { "cmd": "node scripts/check-adr-symbol-anchors.mjs", "exit": 0 },
          { "cmd": "node scripts/check-adr-symbol-anchors.mjs --self-test", "exit": 0 },
          { "cmd": "node scripts/check-ci-filter-parity.mjs", "exit": 0 },
          { "cmd": "node scripts/check-closing-keyword-parity.mjs", "exit": 0 },
          { "cmd": "node scripts/check-closing-keyword-parity.mjs --self-test", "exit": 0 },
          { "cmd": "node scripts/check-comment-mask-corpus.mjs", "exit": 0 },
          { "cmd": "pnpm --filter @objectstack/lint run check:doc-formula-expressions", "exit": 0, "note": "FIRST RUN WAS EXIT 3 = PREREQUISITE NOT MET (@objectstack/formula not built) and measured NOTHING — ⛔ not read as green. Cleared by turbo build --filter=@objectstack/formula --filter=@objectstack/lint under the shared verify lock, then re-run to this 0." },
          { "cmd": "pnpm check:adr-anchors", "exit": 0 },
          { "cmd": "pnpm check:cross-package-test-inputs", "exit": 0 },
          { "cmd": "pnpm check:doc-authoring", "exit": 0 },
          { "cmd": "pnpm check:driver-memory-census", "exit": 0 },
          { "cmd": "pnpm check:nul-bytes", "exit": 0 },
          { "cmd": "pnpm check:pm-governed-merges", "exit": 0 },
          { "cmd": "pnpm check:refd-timer-probe", "exit": 0 },
          { "cmd": "pnpm check:watch-hint-literal", "exit": 0 }
        ],
        "not_measured_declared": [
          "check-cross-package-test-inputs --union-into RUNNER_TEMP/turbo-ls.json --changed RUNNER_TEMP/changed-files.txt — takes a value from the workflow, no value outside a CI run",
          "check-shard-attestation --emit --job test --shard MATRIX --total 6 — same",
          "check-test-completeness RUNNER_TEMP/test-core.log ... — same",
          "10 families whose declared population is too wide to place, and 41 artifact-roster families scored silent for every card in the tree — ⛔ neither group is a clearance; none of their rosters sits in a directory this PR's single path is in"
        ]
      },
      "changeset": "none; skip-changeset label applied and read back. Precedent measured, not recalled: PRs 14976 (ADR-0131), 14151 (ADR-0130), 12519 (ADR-0127) are ADR-only landings with NO .changeset file and the skip-changeset label; 13067 (ADR-0129) carried a changeset because it also changed spec + plugin-auth code. Label write route: container REST probe returned 403 for repo-scoped reads, so the additive REST POST endpoint was unavailable this session; fell back to MCP read (documentation, size/m) → union → whole-set write → comparative read-back, which returned documentation + size/m + skip-changeset with draft still true. Channel switch declared here.",
      "tests": "No code changed, so no package test/typecheck applies; the deliverable's verification IS the gate union above, run at e1e5d0baa and listed command-by-command with the exit code captured after its own redirect. Additionally: every code anchor written into the ADR was opened and read before being written (plugin-auth auth-plugin.ts / auth-manager.ts / objectql-adapter.ts / last-admin-guard.ts / invitation-role-cap.ts / member-role-canonical.ts / register-sso-provider.ts / auth-schema-config.ts / set-initial-password.ts, platform-objects identity sys-user / sys-member / sys-account / sys-sso-provider objects, core resolve-authz-context.ts), and check-adr-symbol-anchors independently proves each anchor resolves to a real declaration site with comments stripped. Control-character self-scan beyond the gate: grep -naP over the new file, zero hits.",
      "mcp_calls": "10 — search_issues x1 (dedup for the finding), issue_write create x1 (#16477), create_pull_request x1, issue_read get_labels x2 (BOTH FAILED: 'Could not resolve to an Issue with the number of 16478' — that method does not resolve a PR number in this session), pull_request_read get x1, list_pull_requests x2 (label read + read-back), issue_write update x1 (labels), add_issue_comment x1 (this comment). REST was 403 on a repo-scoped probe at the start of the run, and gh is absent, so issue/PR body reads went through the zero-quota public-repo page payload instead.",
      "open_questions": [],
      "out_of_scope_findings": [
        "filed as #16477 (label `finding`, unassigned): five plugin-auth comments still point at `last-admin-ban-guard.ts`, a filename that has not existed since the guard was renamed to `last-admin-guard.ts` — invitation-role-cap.ts, objectql-adapter.ts, break-glass-local-credential.test.ts x2, and objectql-adapter.test.ts naming a `last-admin-ban-guard.test.ts` as where the 403 arm is pinned (that file does not exist either). ⛔ Not fixed in this PR: docs/adr/** only."
      ],
      "notes_on_card_and_brief": [
        "CARD RE-CHECK FIGURE IS WRONG AS WRITTEN. The card annotates its second re-check `# expect ~71` on a command that includes `grep -v -i 'cloud ADR-0024'`. At the card's own cited commit 00ff228fe that command yields 59; 71 is what the command yields WITHOUT the filter line. On today's origin/main the filtered figure is 64. Reported unsmoothed; #14361 should re-measure rather than inherit 71.",
        "CARD'S 'next free number / 133 records today; 0130 is the highest' IS STALE and the PM brief correctly overrode it. On origin/main 5a9138703 the highest is 0131, and 0133/0134 are spoken for. Used the PM-assigned 0132 and did not pick a number myself.",
        "PRIORITIZATION.md IS NOT A HAND-MAINTAINED INDEX, so the card's conditional ('register the file there IF that index is maintained by hand — read the file's own header') resolves to NO. Its header, verbatim: '⚠️ STALE (noted 2026-07-16): this review predates ADR-0050 onward ... Treat it as a historical snapshot, not current state. The Status: header of each ADR file (kept current per the hygiene rule below) is the authority; this document is retained for its method and its ranked-plan rationale.' It is a dated 2026-06-12 review of 'All 49 ADRs in docs/adr/'; no ADR from 0050 up is listed. Adding a 2026-09 record to it would falsify a snapshot. docs/adr/ holds no other index file. ⇒ Not edited.",
        "SLUG CHOICE, stated because it is a judgement the card left open: the file is `0132-identity-and-access-architecture.md`, reusing the cloud original's slug. ADR-0079 — the shape the card told me to copy — states that precedent in its own Provenance section ('deliberately reuses the original's slug ... so the two are recognisably one record'). Rename is cheap if the reviewer prefers a `-open-half` suffix.",
        "A DETAIL WORTH #14361'S ATTENTION, recorded in the new ADR rather than assumed: a bare `ADR-0024` in this tree cannot be re-pointed mechanically, because some sites mean a clause that STAYS in cloud — `packages/plugins/plugin-auth/src/auth-manager.ts` cites `ADR-0024 V1` for the SSO default-role provisioning, and V1 is on the card's own stays-in-cloud list. The same trap sits at 0071: cloud ADR-0071 is env-side SCIM, while this registry's 0071 is the dataset semantic-layer depth record. Both are written into the record.",
        "PR BODY COSMETIC, disclosed rather than silently left: one blockquote line in the PR body renders as a literal '>' instead of a quote marker (I HTML-escaped it when passing the body). Text is intact and readable; ⛔ not edited, because editing a PR body downgrades the create-only session-URL footer and risks the draft bit, which on a governed surface is the more expensive mistake."
      ]
    }

    Generated by Claude Code

  4. os-warren commented on Sep 7, 2026

    @os-warren
    Collaborator

    ⛔ PM 更正:我分配的 ADR 号 0132 撞车。改为 0135

    我在认领评论(5564491920)里把本卡的编号定为 0132,并写着「origin/main (5a9138703) 上最高是 0131;且 0133 已被在飞的 PR #16267 占用」。那次测量不完整 —— 我只查了 main 和 #16267,没有扫全部开放 PR。

    实测:PR #16215(claude/issue-16130-open-org-scoping,标题即写着 (ADR-0132))在它的分支上加了

    docs/adr/0132-multi-organization-runtime-is-open-core.md
    

    git diff --name-only origin/main...pull/16215/head 命中。它在 main 上确实是空号,但已被一个开放 PR 认领。⇒ 席位按我的指示写出的 PR #16478(docs/adr/0132-identity-and-access-architecture.md)与它撞。

    ⚠️ 这件事本来是可查的:PR #16267 的正文里就写着 —— "#16215's branch adds docs/adr/0132-…, so 0132 is claimed by an open PR even though it is free on main",而且那位席位当时逐一扫了全部 40 个开放 PR。我派发前没读到它。

    当前占用(实测)

    号 占用者
    ≤0131 已落在 origin/main
    0132 PR #16215(开放)
    0133 PR #16267(卡 #14508,第三份镜像,开放)
    0134 PR #16476(卡 #14507,姊妹卡,开放)
    0135 空 ⇐ 本卡

    已把改号指示发回席位:重命名文件(slug 不变)、更新文件内 1 处自引、改 PR 标题与正文并写明改号原因、重跑 check-adr-anchors / check-adr-symbol-anchors / check:pm-governed-merges。普通提交即可,⛔ 不需要 force-push。

    ⭐ 席位本身没做错,两条已由 PM 实测确认

    • 0 处 path:NNN 行锚点。 这条要紧:check:adr-symbol-anchors 自 2026-09-01 的裁定起把 docs/adr/** 下任何存活的行锚点判为硬失败、无过渡期(背后普查测得 337 个行锚点中 243 个已失效)。⚠️ 而本卡正文供的三个锚点是 file.ts:NN 形式 —— 席位没有照抄,是对的;我的派发简报也没提醒这条,应当提醒。
    • 没有引用姊妹镜像尚未落地的号(0133 / 0134)。引用 docs/adr/ 下不存在的号会被 check-adr-anchors 判为 squat 而失败。

    ⛔ 受管面不变:PR 保持 draft、人工合并、不 arm auto-merge。


    Generated by Claude Code

  5. github-actions commented on Sep 7, 2026

    @github-actions
    Contributor

    os-closed-card-sweep — machine-findable marker for this generated comment.

    Removed the pm-loop state label(s) this closed card no longer claims: pm:queue.

    A state label claims work is in flight. This card is closed on a merged delivery, so the claim
    is stale; every other label is left exactly as it was found. Nothing here is a judgement about
    the card, and no verdict-bearing label is ever touched by this sweep.

    posted by half-state-patrol run 34097314575 · trigger schedule

    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions