fix: workspace rules state lives outside the project

Merged
SA
6h ago
+276-45

Issue for this PR

No public issue on purpose: this fixes a security bug in the released v0.12.9, and it is tracked internally until a fixed release ships. A maintainer can confirm.

Type of change

  • Bug fix
  • New feature
  • Refactor / code improvement
  • Documentation

What does this PR do?

The workspace rules state was read from .altimate-code/learn/workspace-rules/rules.json in the working tree and acted on with the signed-in person's credential:

  • its queued proposals were sent;
  • its pending approvals were replayed, including "approve anyway";
  • its review notices were shown.

The file is git-ignored, but a branch, a merge or a pull can still bring one in. A state with no scope was adopted as is. In a workspace owner's client, a planted outbox entry or approval became an active team rule for every member. This affects main since #1605 and the v0.12.9 release.

The fix stops trusting anything about workspace rules that comes from the repository:

  • The state lives outside the project, at $XDG_STATE_HOME/altimate-code/workspace-rules/<sha256 of the project's real path>.json (usually ~/.local/state/...). The file is 0600 and its directory 0700. A rules.json in the project is never read.
    • The path is in rules/state-path.ts, which the store loads lazily, because importing @/global at load time is circular.
    • It uses the same key as the workspace link cache (the folder's real path), so two clones or worktrees of one repository each keep their own state.
  • Team rules are delivered from that state. RulesStore.vouched builds each rule's lesson (wording, pin, tags, paths) from the state, and delivery gives those. The workspace-rules/approved.json copy in the project decides nothing that is delivered. A resumed session keeps a cached team rule only while the state still holds its id.
  • Only for the account that pulled them. A pull records the credential it ran with (pulled_with). Rules are vouched for only while that credential is signed in, so a sign-out or a switch to another account stops them at once instead of at the next sync.
  • session/prompt.ts: the "can this project have team rules" check looks for the state file in its new location.

No migration, on purpose. An old in-project rules.json is exactly what must not be trusted. After upgrading, a client re-pulls its team rules on the next sync. These, stored by v0.12.9 in the old location, are not carried over:

  • proposals queued but never sent;
  • dismissed topics, and the personal learning choice: someone who turned learning off for themselves in /rules is back on the workspace setting;
  • which usage was already reported: the first report after upgrading can count sessions of the last 30 days again, so "applied in N sessions" and "corrected again" can be inflated once;
  • session snapshots are kept as they are. A session that ran on v0.12.9 and froze a team rule's wording keeps it on resume while the state holds that rule's id.

Not covered here:

  • Recurrence analysis and learn show still read the project's team-rules copy. A planted copy can only affect local analysis: it can make a correction topic look covered, which suppresses a proposal, and learn show would list it. It is never sent or delivered.
  • Usage reporting is still built from the project's .sessions snapshots and recurrence signals. A branch that commits snapshots can inflate the "applied" and "corrected again" counts sent with the person's credential. Counts only: nothing is proposed, approved or delivered.
  • The learn lock directory is still in the project, as for every learn store. A lock directory committed to a branch would look held after checkout and block learn writes in that project until it goes stale (10 minutes).

The Claude Code and Codex plugin PRs (AltimateAI/altimate-claude-plugin#13, AltimateAI/altimate-codex-plugin#7) depend on this fix, and #1626 rebases onto it.

How did you verify your code works?

  • bun run typecheck and the marker guard (analyze.ts --markers --strict).
  • New tests:
    • a state planted in the project, with a queued proposal marked for approval, a pending approval and a future-dated notice, is not read, and push makes no request (every fetch counted);
    • the real state file is outside the project and private;
    • a planted team-rules copy, or one whose wording differs from the state's, is not delivered;
    • a copy that keeps an approved rule's id and wording but changes its pin or paths is delivered as the state holds it;
    • a resumed session drops its cached team rules while the rules state can't be read;
    • mayHaveWorkspaceRules counts a state in the new location, with no workspace binding, and ignores a rules.json in the project;
    • team rules stop at once after a sign-out or a switch to another account, cached section included;
    • a pull that finishes after someone else signed in is not stamped with its credential.
  • rules, learn, rules-routes and learn-prompt suites: 2201 pass, 0 fail. A wider run (session, server, cli, telemetry) had 9 failures, which also fail on a clean main locally or pass alone (timeouts under load): experimental and mcp HttpApi, --time-budget, serves read routes, held-back promotion.

Cost side effects

None. No model calls, queries or cloud resources are added. The credential check reads the local credential file.

Screenshots / recordings

Not a UI change.

Checklist

  • I have tested my changes locally
  • I have not included unrelated changes in this PR
  • I checked for cost side effects (model calls, queries or cloud resources that grow with user data) and described them, or wrote "none"

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes
    • Workspace rules are delivered only while signed in to the account that synced them. Project copies alone do not qualify, and delivery stops after sign-out or an account change.
    • Project-local rule files no longer determine workspace-rule settings or which team rules are delivered.
  • Documentation
    • Clarified that workspace rules and the /rules learning choice are stored in user state outside the project.