Skip to content

docs(adr-0069): D3 states what the gate holds — the account's standing, so an API key answers as the session does (ruling C′ item 5) - #23472

Merged
os-zhuang merged 1 commit into
mainfrom
claude/issue-23287-adr-0069-d3
Oct 14, 2026
Merged

os-zhuang merged 1 commit into
mainfrom
claude/issue-23287-adr-0069-d3

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Part of #23287
Clause-②: no

This PR writes ruling C′ item 5 into ADR-0069 D3. The ruling is on #23287 (comment 6159450584, the maintainer's 「同意」). The sentence also records the default for the forced-reset revocation, which the maintainer set as letter A (record 6165848580). The card's code is PR #23440. This PR contains no code.

Tier H (docs/adr/**). The PR stays a draft until the maintainer approves it. Before that approval, no seat marks it ready, queues it or arms auto-merge.

Clause-②: no: this diff adds nothing to a published payload and publishes no package. The claim's Clause-②: yes line belongs to the code PR #23440. Changeset: none. docs/adr/** publishes nothing, so the PR carries the skip-changeset label.

The change: one file, +12 / −1

docs/adr/0069-enterprise-authentication-hardening.md changes in two places. Nothing else in the record changes:

  1. D3's note. The note under the D3 table ends with "Gate at the session-validation seam so a half-authenticated (password-only) session cannot read data." It now has one more paragraph: a bold provenance label and one sentence.
  2. The status line gets one · **Amended** entry that points to that note.

1. D3: the new paragraph (exact)

> **What the gate holds (amended 2026-10-14: ruling C′ on [#23287](https://github.com/objectstack-ai/objectstack/issues/23287),
> [`6159450584`](https://github.com/objectstack-ai/objectstack/issues/23287#issuecomment-6159450584), maintainer 「同意」;
> the revocation's default is the maintainer's letter A,
> [`6165848580`](https://github.com/objectstack-ai/objectstack/issues/23287#issuecomment-6165848580)).**
> The gate holds the account's standing and an API key answers as the session does (a held
> user's key is refused with the session's code and remediation, and admitted again once the
> standing clears: a refusal, not a revocation); password expiry (D1) holds password sign-in
> alone, so the user's keys keep working; and an administrator's forced password reset (D7)
> carries an explicit revocation of the user's sessions and API keys, on by default and off only
> as the administrator's explicit act.

2. Status line: the appended entry (exact)

· **Amended** (2026-10-14, ruling C′ on [#23287](https://github.com/objectstack-ai/objectstack/issues/23287), [`6159450584`](https://github.com/objectstack-ai/objectstack/issues/23287#issuecomment-6159450584), maintainer 「同意」) — D3 states what the gate holds for an API key, for password expiry and for a forced reset; see the note under D3.

How each clause maps to the ruling

Clause in the sentence Ruling text it carries
"The gate holds the account's standing and an API key answers as the session does" Item 5: "the gate holds the account's standing and a key answers as the session does"
"(a held user's key is refused with the session's code and remediation, and admitted again once the standing clears: a refusal, not a revocation)" Item 4: "The refusal is not a revocation: the key resumes the moment the user enrols (or the account posture clears); the 403 body carries the posture code and the remediation pointer the session gets."
"password expiry (D1) holds password sign-in alone, so the user's keys keep working" Item 5: "password expiry holds the password alone"; item 1: "it holds password sign-in alone; the user's keys keep working"
"an administrator's forced password reset (D7) carries an explicit revocation of the user's sessions and API keys, on by default and off only as the administrator's explicit act" Item 5: "a forced reset carries an explicit revocation"; letter A (6165848580): revokeSessionsAndKeys stays ON by default for every forced reset, and turning it off is the administrator's explicit act

Why it is placed and worded this way (measured on main at 1685cd5ca7)

  • Placement. D3 states what the gate holds in two places: its mfa_required row and the note under the table. The note names the seam and why it is there, so the sentence continues the note rather than widening a table cell. The sentence cites D1's password_expiry_days row and D7's "force password reset" action by section name. Neither one is edited.
  • Provenance convention. This record already writes its one earlier settled question in two places: the status line ("Session-of-record question settled 2026-08-04 (decision(plugin-auth): 会话的「记录之处」到底在哪 —— better-auth secondaryStorage 一旦接上 cache,ADR-0069 D4 的会话管控就静默失效 #4785)") and bold-led notes inside D2 and D4. Sibling amendments on main use a · **Amended** (date, ruling …) entry on the status line plus the text in place: ADR-0138 D3 item 6 (34d144ca4d), ADR-0005 and ADR-0043. This PR follows both conventions. The status entry is a pointer and does not repeat the decision, so the two texts cannot drift apart.
  • True as a decision record, whichever PR lands first. The sentence names no symbol that exists only in PR feat(spec,core,plugin-auth)!: an API key answers as its owner's session does when the ADR-0069 policy holds the account (#23287) #23440. On main, PASSWORD_RESET_REQUIRED has 0 hits under packages/, and the sentence does not name AUTH_GATE_SCOPES or resolveUserAuthGates. "The account's standing" and "password sign-in alone" are the ruling's scope words (item 1), written as prose rather than as code identifiers.
  • Prime Directive 13 grep. Before this PR, ADR-0069 mentioned API keys 0 times. Across docs/adr/, a case-insensitive grep for authGate|un-stepped-up|MFA_REQUIRED|PASSWORD_EXPIRED hits ADR-0069 and two other places: a field list in ADR-0092 and an allowlist predicate in ADR-0138. Neither rules on a held user's key. ADR-0101 (stdio key admission) runs a key "under posture rules (ADR-0099)", and this sentence does not contradict that.

Gates

On head 38a672e9b6, node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack was run with no paths. It derived the change set from the merge base 1685cd5ca (1 path, +12 / −1) and listed 19 commands. All 19 ran, each exit code was captured before any pipe, and all 19 exited 0. One of them, pnpm --filter @objectstack/lint run check:doc-formula-expressions, first exited 3 with PREREQUISITE NOT MET because @objectstack/formula and @objectstack/lint were not built. That run measured nothing. After a build of those two packages under os-verify-lock (VERDICT command-exit 0), the gate re-ran and exited 0. The --ran reconciliation with the exit codes recorded printed: 19 derived famil(ies) accounted for — 19 run, 0 NOT-MEASURED (a DERIVED zero — all 19 recorded an exit code and none of them is 3).

The gates' own verdict lines:

  • pnpm check:adr-anchors: OK (68 anchored file(s), every governing ADR still referenced; 136 decision number(s), …; 46287 citation(s) across 6051 file(s) resolve; …)
  • node scripts/check-adr-links.mjs: 761 relative link destination(s) under docs/adr/ resolve
  • node scripts/check-adr-symbol-anchors.mjs: 2347 anchors across 142 records resolve
  • pnpm check:doc-authoring: clean on all three axes
  • pnpm check:nul-bytes: no raw ASCII control bytes
  • node scripts/pm/check-governed-merges.mjs --branch claude/issue-23287-adr-0069-d3: exit 3, which is this tool's governed verdict and not a failure: landing tier: H(人合), docs/adr/** ×1, 13 changed line(s) (+12 / -1) ≤ 3000

These run in CI, not locally: the lint workflow's full set of checks, link checking, and the PR-body guards (Part-of / closing keyword, closing-target claim, duplicate fix). The derivation itself names 15 families that apply only once a changeset exists. This PR has none: it carries the skip-changeset label.

Acceptance notes

维护者速读(草稿)

Drafted by the os-dev agent that domain:spec seat 4 dispatched, on the claim 6165524186 (branch claude/issue-23287-adr-0069-d3).


Generated by Claude Code

…g, so an API key answers as the session does; password expiry holds password sign-in alone; a forced reset carries an explicit revocation (ruling C′ item 5)

One sentence under D3's note, with the ruling's provenance on the status
line. Records ruling C′ item 5 and the maintainer's letter A on the
forced-reset revocation default. No other text in the record changes.

Claude-Session: https://claude.ai/code/session_0121ooBYRmS8ziFXm9ai5YHm
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet objectstack-fleet Bot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Oct 14, 2026
@github-actions github-actions Bot added the documentation Improvements or additions to documentation label Oct 14, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

维护者速读

  • 改了什么: 只改 ADR-0069 一个文件(+12 / −1),不改代码。D3 下面加了一句话,状态行加了一条「Amended」指针。这句话是:这道闸扣的是账号的状态,API key 和会话给出同一个答案;被扣用户的 key 收到和会话相同的拒绝码和补救指引,账号恢复合规后 key 自动恢复(这是拒绝,不是吊销)。密码过期只扣密码登录,key 照常可用。管理员强制改密时附带一次明示的吊销,吊销该用户的会话和 API key,默认开,只有管理员明确关掉才不吊销(你选的 A)。
  • 为什么改: 裁决 C′ 第 5 项要求这一句。ADR 原文只讲「会话」,对 API key 只字未提。记录不补上,后来的人照原文读,会认为闸只管会话,把 PR feat(spec,core,plugin-auth)!: an API key answers as its owner's session does when the ADR-0069 policy holds the account (#23287) #23440 的修复当成越权改回去。
  • 风险与代价(含回滚): 纯文档,不进任何发布包,不改运行时行为。这句话写的是决定,不是「已实现」:它先于或后于 PR feat(spec,core,plugin-auth)!: an API key answers as its owner's session does when the ADR-0069 policy holds the account (#23287) #23440 合入,文字都成立。状态表没有改成「已实现」。回滚只需 revert 这一个提交。
  • 席位意见: 建议批准。措辞忠实于裁决第 5 项原文和你的 A,只多写了「拒绝不是吊销」(裁决第 4 项)。dev 问是否同时改状态表,本席取不改:一句话的授权就是一句话。
  • 你要做的: 读 D3 下面新增的那一句,同意就用 os-zhuang 或 hotlong 在本 PR 上 Approve;要改措辞,直接在 PR 上写原话。

席位核对

domain:spec seat 4 (#18917) · sunhaolin · session session_0121ooBYRmS8ziFXm9ai5YHm · claim 6165524186 on #23287 · reviewed head 38a672e9b6c5a923040ef45e107a4a3cdf2b0512 · 2026-10-14T11:50Z

Verdict: ACCEPT. Tier H (docs/adr/**): it lands only on an authorized approval, plus a CONTRACT_REVIEW_TIER record (governed rules text) and green checks.

  1. Shape. Draft, first line Part of #23287 (not Fixes: PR feat(spec,core,plugin-auth)!: an API key answers as its owner's session does when the ADR-0069 policy holds the account (#23287) #23440 closes the card), Clause-②: no (this PR publishes nothing; the claim's yes line belongs to PR feat(spec,core,plugin-auth)!: an API key answers as its owner's session does when the ADR-0069 policy holds the account (#23287) #23440), skip-changeset. One file: docs/adr/0069-enterprise-authentication-hardening.md, +12 / −1.
  2. Against the ruling. The D3 note carries ruling C′ item 5 sentence for sentence (the gate holds the account's standing, and a key answers as the session does; expiry holds password sign-in alone; a forced reset carries an explicit revocation). It adds item 4 (a refusal, not a revocation) and letter A's default (6165848580). The status line gains a provenance pointer only. It names no symbol that exists only in PR feat(spec,core,plugin-auth)!: an API key answers as its owner's session does when the ADR-0069 policy holds the account (#23287) #23440, so it reads true as a decision record whichever PR lands first.
  3. Gates. The dev ran all 19 derived families on 38a672e9b6, all exit 0 (check:adr-anchors and check-adr-symbol-anchors among them). CI check-runs were converging at review.
  4. Open question. Whether to also edit the P1 status row: the seat takes A, leave it. The ruling authorizes one sentence, and the status table claims nothing about this rule.
  5. Out-of-scope. carrier: none, noted and not filed: the role file's Clause-② copy rule does not scope one claim line across several named branches. This PR took the per-PR reading. A protocol observation, routed to this seat's shift report.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 38a672e9b6c5a923040ef45e107a4a3cdf2b0512
Local-runs: none

Face: governed rules text, Tier H (docs/adr/**, one path). Inputs: card #23287's body and all 17 comments (ruling C′ 6159450584, letter A 6165848580, the correction 6165921805, the dev report 6167636585); PR #23472's body, file list and net diff against main (three-dot diff from merge base 1685cd5ca7, byte-identical to the API diff: one file, +12 / −1, one commit); the 47 check-runs on the head. The seat's own PR comment (6167674771) was not read.

① Derived judgments

  1. The sentence says what ruling C′ item 5 decides, with item 4 and letter A, and nothing more. Clause by clause against the ruling text: (a) "The gate holds the account's standing and an API key answers as the session does" is item 5's first clause as prose; (b) the parenthetical "a held user's key is refused with the session's code and remediation, and admitted again once the standing clears: a refusal, not a revocation" is item 4 (the 403 carries the posture code and the remediation pointer the session gets; the key resumes when the posture clears); (c) "password expiry (D1) holds password sign-in alone, so the user's keys keep working" is item 5's second clause in item 1's own words; (d) "an administrator's forced password reset (D7) carries an explicit revocation of the user's sessions and API keys, on by default and off only as the administrator's explicit act" is item 5's third clause plus item 2's switch, with the default as letter A set it (6165848580 part 1: on for every forced reset, off only as the administrator's explicit act; the ruling's "when the reason is a suspected compromise" condition is correctly absent, since letter A ruled no reason field). Nothing in the sentence lies outside those sources: no scope-enum words, no PASSWORD_RESET_REQUIRED, no seam or symbol from PR feat(spec,core,plugin-auth)!: an API key answers as its owner's session does when the ADR-0069 policy holds the account (#23287) #23440 (item 1's enum, item 2's code and item 3's seam belong to the code PR, not to the sentence). The correction 6165921805 (a revoked key can be restored) bears on PR feat(spec,core,plugin-auth)!: an API key answers as its owner's session does when the ADR-0069 policy holds the account (#23287) #23440's changeset, not on this sentence, which makes no finality claim about revoked keys.
  2. No conflict with the rest of ADR-0069. D1's password_expiry_days row and seam-summary item 4 place password expiry at the session-validation seam, returning PASSWORD_EXPIRED with a forced /change-password; the amendment's "holds password sign-in alone" reads "authenticated requests" there as session-authenticated and contradicts nothing (a key cannot change a password). D7's "force password reset" action is the one the sentence names. D4's revocation mechanism (stamp sys_session.expires_at) is what "revocation of the user's sessions" means and is untouched. The sentence sits under the D3 note that already names the seam and its purpose, which is the right anchor: D3's mfa_required row and that note are the two places the record states what the gate holds.
  3. No conflict with any other ADR (Prime Directive 13 grep over docs/adr/ on main, gate and key terms). Beyond the two the dev named (ADR-0092's system-managed stamp list naming must_change_password; ADR-0138's isAuthGateAllowlisted predicate), three more records touch the surface and none rules the other way: ADR-0036 (key admission is hash match, not revoked, not expired, then "run as that principal under the existing permissions, never an escalation"; it states no exclusivity, and a key escaping a gate the session is held by would be the escalation it forbids); ADR-0090's rule-4 note (a credential is how a principal acted, never a second who; a key resolves its owner and nothing else), which is the same reading as "the account's standing"; ADR-0101 (the stdio key goes through the one shared verify chain, re-resolved per call "so revocation applies to a live session"; its "posture rules (ADR-0099)" are tenant-tiering posture, a different word). ADR-0095 and ADR-0105 use "posture" in the tiering sense only. No ADR rules that the gate holds the session alone.
  4. True as a decision record before PR feat(spec,core,plugin-auth)!: an API key answers as its owner's session does when the ADR-0069 policy holds the account (#23287) #23440 lands. The sentence is a decision dated 2026-10-14 under a status-line entry of the same date; the implementation-status table is headed with its own date (2026-07-04) and claims enforcement of that scope only (computeAuthGate yielding MFA_REQUIRED); nothing in the amended text claims the key rule is enforced. On main the premises hold as the body states: PASSWORD_RESET_REQUIRED has 0 hits under packages/, the gate codes are PASSWORD_EXPIRED (auth-manager.ts 8113, 8124) and MFA_REQUIRED (8147), restore_api_key exists (sys-api-key.object.ts:105), and ADR-0069 mentioned API keys 0 times before the edit. The sibling convention (ADR-0138's two amendments of 2026-10-13 and 2026-10-14, ADR-0131) records a maintainer's decision first and lands enforcement through cards after; this PR follows it. Whichever PR lands first, the text stays true.
  5. Provenance is in the record's own convention. The status-line entry has the exact shape of ADR-0138's two amended entries (date, "ruling C on" the card link, the comment-id link, the maintainer's assent word, then the D-section it amends) and of the inline entries on ADR-0005, ADR-0017 and ADR-0043; it points to the note and restates no decision, so the two cannot drift. The note's bold-led label mirrors this record's own "decision(plugin-auth): 会话的「记录之处」到底在哪 —— better-auth secondaryStorage 一旦接上 cache,ADR-0069 D4 的会话管控就静默失效 #4785" notes under D2 and D4. The label cites both governing comments (ruling C′ and letter A); the status entry cites the ruling, which is enough for a pointer. Links are absolute, so check-adr-links (relative links only) is unaffected; the added text has no tag-shaped fragment.
  6. Shape and governance. One governed path (docs/adr/**), landing tier H; the PR is a draft, auto-merge unarmed, needs-user-decision on, review requested from the two governed approvers; head repo equals base repo. The commit carries the model-free trailer pair AGENTS.md prescribes; no model identifier appears in the title, body, commit or diff. Check-runs on the head, read 2026-10-14T11:57:17Z after everything else: 47 runs, 32 success, 15 skipped, 0 failure, 0 in_progress. Success includes Governed Surface Queue Guard, Check Changeset, Check Documentation Links, Part-of PR must not also close its card, The card this PR closes must claim this branch, both claim-uniqueness checks (same issue, same single-writer path), every Lint, Type Check and Test Core leg, and Dogfood Regression Gate. Skipped: Build Core, Build Docs, Console Pin Gate, Dogfood Verify CLI, the Dogfood shard matrix, Temporal Conformance, Packed-tarball smoke (opt-in, three instances), and second instances of Auto Label, Check Changeset and Check PR Size whose first instances succeeded. This PASS is the contract-tier record only: Tier H lands on the maintainer's authorized APPROVED review, which no seat may substitute for.

② Semver level

None: nothing publishes. The diff is one docs/adr/** file, outside every released package, so skip-changeset is the right label (AGENTS.md Post-Task Checklist step 3: that label is for a diff that publishes nothing from any released package) and Clause-②: no is the right declaration: yes takes at least minor, which a diff with no changeset and no published payload cannot carry. No .changeset/*.md is present and none is owed; Check Changeset succeeded. The claim's Clause-②: yes line (6165524186) belongs to the code PR #23440, where the widening and the narrowing live; carrying it here would have been a false declaration (see ③).

③ Boundary flags

Dev report 6167636585, branch claude/issue-23287-adr-0069-d3.

Implemented-by: claude/issue-23287-adr-0069-d3
Reviewed-by: session_0121ooBYRmS8ziFXm9ai5YHm

VERDICT: PASS

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

Labels

documentation Improvements or additions to documentation size/s skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants