Skip to content

Validate the members file fully before an access change writes it - #126

Merged
NinjaLikesCheez merged 4 commits into
mainfrom
fix/access-store-validate-before-write
Oct 19, 2026
Merged

NinjaLikesCheez merged 4 commits into
mainfrom
fix/access-store-validate-before-write

Conversation

@NinjaLikesCheez

Copy link
Copy Markdown
Contributor

Needs a human reviewer (AGENTS.md rule 14): this changes src/services/access-store.ts.

What was wrong

FileAccessStore.apply() checked the current members.yaml only against the per-entry schema (parseMembers). It then wrote the new file and only after that called buildAccessConfig, which runs the cross-file checks (an id in two entries, an admin with no members entry).

Example: someone hand-edits members.yaml so the same discord:<id> is in two entries, or removes the admin's entry. The schema passes. An admin runs /admin level set:

  1. the new members.yaml is renamed into place (with a .bak),
  2. buildAccessConfig throws AccessConfigError (not user-facing, and its message includes the file path),
  3. the in-memory view isn't swapped and no access.changed audit log or breadcrumb is written,
  4. the admin gets a generic error, and a retry says "nothing changed" because the no-op check reads the file that was already written.

This broke the guarantee in docs/identity-and-access.md ("If any step fails, both the file and the view are unchanged") and AGENTS.md rule 10 (every change is audited).

What changed

  • #parseCurrent now also runs buildAccessConfig(this.#admins, entries, paths), the same checks as at startup. An AccessConfigError from it maps to the existing AccessStoreError: "The members file is invalid, so nothing was changed. Fix it, then run /admin reload." It is logged as access.file_invalid.
  • The next view is built before #writeFile, and swapped in only after the write succeeds. With the current file cross-checked, our own edit can't fail here (adding a new entry only happens when the id isn't already present), but if it ever does, nothing is written.
  • The class comment and docs/identity-and-access.md steps 1 and 3 now describe this. There is a CHANGELOG Fixed entry.

Testing

  • New parameterised test in src/services/access-store.test.ts covers both a duplicate id across entries and an admin with no entry. For each one it tries a new-person set-tier, an existing-person set-tier and a set-capabilities, and asserts that each is refused with an AccessStoreError (no ID in the message), the file is byte-for-byte unchanged, there is no .bak, the view is the same object, and nothing is audited.
  • Both cases fail on main: the error is an AccessConfigError, not an AccessStoreError. Both pass with the fix.
  • just check passes (lint, typecheck, tests, coverage thresholds).

🤖 Generated with Claude Code

apply() only ran the per-entry schema on the current file, wrote the new
file, and then built the view, which runs the cross-file checks (an id in
two entries, an admin with no entry). A hand edit that failed those checks
left the file written but the view and audit record unchanged, with a
generic error for the admin. Run the startup cross-checks on the current
file and build the new view before writing, so the change is refused and
nothing is written.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@NinjaLikesCheez NinjaLikesCheez added bug Something isn't working security Auth, access control, privacy, secrets — needs human review labels Oct 18, 2026
@NinjaLikesCheez
NinjaLikesCheez merged commit b18447d into main Oct 19, 2026
7 checks passed
@NinjaLikesCheez
NinjaLikesCheez deleted the fix/access-store-validate-before-write branch October 19, 2026 07:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working security Auth, access control, privacy, secrets — needs human review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant