# For AI agents (and the people driving them)

Increasingly the "reader" of authorization docs is an AI coding agent. These
prompts encode the foot-guns this documentation already knows about, so an
agent gets the integration right on the first try. Copy, adjust the names,
paste.

There is an [`llms.txt`](llms.txt) index of this site, a one-file
[`llms-full.txt`](llms-full.txt) corpus, and a Markdown twin for every HTML
page (same path, `.md` suffix, advertised as `rel="alternate"`). The deep
guides under `docs/guides/` are published on this site from that same tree —
do not treat raw GitHub as a second canon.

## Install CurrentScope in this app

```text
Add the current_scope gem to this Rails app (Rails >= 8.1, < 9 required).

1. Add `gem "current_scope"` to the Gemfile, then run:
   bin/rails generate current_scope:install
   bin/rails current_scope:install:migrations && bin/rails db:migrate
2. In ApplicationController, include CurrentScope::Context and
   CurrentScope::Guard (in that order). Authentication must be wired
   BEFORE these concerns run — Context reads current_user when its
   callback fires, so an auth callback registered after these includes
   means the gate denies before authentication happens.
3. The gate is fail-closed and covers EVERY action. You MUST add
   `skip_before_action :current_scope_check!` to controllers where
   authorization does not apply — sign-in/sessions (or nobody can log in),
   webhooks, health checks. A skipped controller is unprotected by the
   permission gate: keep or add the app's own auth there. If the app has
   act-as/impersonation, ALSO add
   `skip_before_action :current_scope_mutation_guard!` on sign-in,
   sign-out, and the stop-impersonation action — that guard is a separate
   callback that survives the gate skip, and without its skip an
   impersonating admin cannot sign out or stop impersonating.
4. If this app already has users and traffic, set
   `config.enforcement = :report` in the initializer BEFORE deploying,
   run the test suite, then run `bin/rails current_scope:report` and seed
   the roles it names. Flip back to :enforce only when re-exercising the
   app adds no NEW access.would_deny rows (the report reads the
   append-only ledger — historical rows never clear) and any
   access.sod_blind_spot rows are resolved. Report mode must not be the
   final state.
   If config.sod_actions is set, ALSO clear any access.sod_initiator_missing
   rows before flipping. Those are not denials — they are 500s. An SoD action
   reaching a model with no current_scope_initiator RAISES
   CurrentScope::ConfigurationError, in :report mode exactly as in :enforce,
   so granting will not clear them. Best done before enabling SoD at all:
   the engine logs a preflight warning naming the ones it can see (at boot in
   production; on the first request in development, whose routes are lazy),
   `bin/rails current_scope:report` lists them, and
   `CurrentScope::SodPreflight.scan` returns them programmatically: a Result
   with .rows ([permission, model_class] pairs), .inspected, .in_scope and
   .skipped. An EMPTY .rows is NOT an all-clear on its own — check
   `result.blind?`, which is true when a check failed (.degraded?) or when
   nothing was read at all (.inspected == 0), because the scan only sees
   controllers declaring current_scope_model.
5. Bootstrap the first admin (the management UI only admits full-access
   subjects): `bin/rails current_scope:grant SUBJECT_ID=<id>` — or, in
   seeds, `CurrentScope.grant!(user)` (it creates the default
   Owner/Member roles if missing; Member starts with zero permissions).
6. Declare record hooks as PRIVATE controller methods. For member actions:
   `def current_scope_record = (set_thing if request.path_parameters[:id])`
   — key off request.path_parameters, never params, so a ?id= query string
   cannot smuggle a record into collection actions. Use the route's actual
   member key: a route declared with a custom param (e.g. :slug) must
   check request.path_parameters[:slug], or the hook returns nil and the
   SoD veto is silently skipped on that action.
7. Run the full test suite. Expect controller tests to 403 until grants are
   seeded — use the test helpers (grant_role!/grant_scoped_role!) from
   current_scope/test_helpers, not stubs.

## Carry role definitions between environments

```text
Export and diff are safe for an agent to run. Import and rollback are not
autonomous in production.

1. Export live roles:
   bin/rails current_scope:definitions:export FILE=config/current_scope/roles.yml
2. Commit the YAML. Diff against another environment:
   bin/rails current_scope:definitions:diff FILE=config/current_scope/roles.yml
3. A human reviews the printed diff. The document is desired state for role
   names, descriptions, full_access, and permission keys. It does not carry
   assignments.
4. A human applies, never an agent on its own:
   bin/rails current_scope:definitions:import FILE=config/current_scope/roles.yml CONFIRM=1 ACTOR_ID=<id>
   Production always needs CONFIRM=1. A populated non-production database
   does too. Do not set CONFIRM=1 unless a human asked in that turn.
5. Rollback reads the snapshot written beside the document, not FILE=:
   bin/rails current_scope:definitions:rollback SNAPSHOT=config/current_scope/roles.yml.pre.yml CONFIRM=1 ACTOR_ID=<id>
```

Before finishing, read docs/SECURITY-CHECKLIST.md in the gem repo
(https://github.com/davidteren/current_scope/blob/main/docs/SECURITY-CHECKLIST.md)
and verify each item that applies.
```

## Declare subject identity and attach Owner

```text
Declare how a subject is identified so grants can be found across
environments. This is config.subject_identity, NOT config.subject_label.
Label is display-only and fail-soft. Identity is load-bearing: duplicate
keys raise, and resolve never inserts a subject.

1. Pick the identity:
   - one column: config.subject_identity = :email
   - composite: config.subject_identity = [:name, :email]
   - split across tables: bin/rails generate current_scope:identity
     then set config.subject_identity = CurrentScopeSubjectIdentity.new
     and fill identify / resolve.
2. Check uniqueness against live rows:
   bin/rails current_scope:identity:check
   The task exits non-zero and names the colliding keys; it does not raise.
   A collision raises ConfigurationError at BOOT, and identity:setup stops
   before it writes. Do not first-row-win.
3. Dry-run a grant (writes nothing):
   bin/rails current_scope:identity:setup SUBJECT=you@example.com
   SUBJECT is whatever your identify returns. For a composite it is a YAML
   sequence: SUBJECT='[Ada, you@example.com]'.
   ROLE= defaults to Owner (full_access). Admin, if you create it, is not
   full_access unless you set that yourself.
   IDENTITY=email is an OVERRIDE for one run, for the case where
   config.subject_identity is not written yet. Do not add it once step 1
   is in the initializer: it replaces the configured identity, so under a
   composite or an object identity it audits and grants by the wrong key,
   and it removes the create_placeholder! factory step 5 needs.
4. Write only after the dry-run looks right:
   bin/rails current_scope:identity:setup SUBJECT=you@example.com WRITE=1
   That calls CurrentScope.grant! (ledger source: bootstrap). WRITE=1 also
   creates the Role row if it does not exist, and seeds the default Owner
   and Member roles when ROLE is unset. The dry-run plan names both.
5. Missing subject: never invent one in production. Outside production,
   PLACEHOLDER=1 needs a create_placeholder! factory on the identity
   object from `bin/rails generate current_scope:identity`. With one, a
   dry-run prints the would-create line; without one the task stops and
   says PLACEHOLDER=1 has no factory. Create the marked row with
   PLACEHOLDER=1 WRITE=1. Production + PLACEHOLDER=1 is refused.

Hard stop: never invent a production subject.
```

## Enable separation of duties on an approve flow

```text
Enable CurrentScope separation of duties so an initiator can never approve
their own record. SoD is OFF by default — config.sod_actions = [] means the
veto never runs.

1. In config/initializers/current_scope.rb set
   `config.sod_actions = %w[approve]` (action NAMES, not full keys).
2. On the model, define `def current_scope_initiator = <initiator assoc>`.
   If an SoD action reaches a record whose class lacks this hook, the
   resolver raises ConfigurationError — that is intended (fail loud).
3. CRITICAL: the controller's `current_scope_record` hook MUST return the
   record on the SoD member action. If it returns nil there, the veto is
   silently SKIPPED and the initiator can approve. This is the load-bearing
   control — verify it, don't assume it.
4. Write the verification test and run it:
   - grant the initiator a role that ticks approve
   - POST the approve action as the initiator
   - assert response :forbidden AND
     response.headers["X-Current-Scope-Reason"] == "sod_veto"
   If the reason is "no_grant" instead, SoD is not examining this action.
5. Do NOT add bypass logic unless explicitly asked. If a conditional
   self-approval is a real requirement, use the engine's break-glass
   (allow_sod_bypass) — never a hand-rolled branch, because break-glass
   records the sod.bypassed audit event a hand-rolled branch forgets.
   That recording requires config.audit enabled (the default); if an
   unaudited bypass must be impossible, set config.audit = :strict.
```

## Debug a CurrentScope 403

```text
A request is being denied. Diagnose it from the engine's own signals; do not
guess or bypass the gate.

1. Read the X-Current-Scope-Reason response header (or the INFO log line
   "[CurrentScope] denied <key> (<reason>) → 403"). Map the reason:
   - no_grant          → nothing grants this controller#action key; check
                         the role grid / seed the grant
   - sod_veto          → the subject initiated this record; that denial is
                         the anti-fraud control working — do not "fix" it
   - model_undeclared  → the controller needs `def current_scope_model = <Model>`
   - model_invalid     → current_scope_model returned something that is not
                         a concrete ActiveRecord class
   - impersonation_gate→ non-GET/HEAD while impersonating; sessions are
                         read-only by design
   - not_full_access   → the management UI; only full-access subjects enter
2. In development/test, check the log for the engine's nudges (nil SoD
   record, inert scoped grant, cross-controller key derivation) — each
   names its one-line fix.
3. If there is no header and no log line, a host rescue_from may have
   replaced the denial handler, or the controller never included Guard —
   run `bin/rails current_scope:ungated` to check the gated surface.
```

## Migration tooling (shipped)

Migrating from Pundit, CanCanCan, or Action Policy? The
[`current-scope-migrate`](https://github.com/davidteren/current_scope/tree/main/.claude/skills/current-scope-migrate)
Claude Code skill is complete ([#45](https://github.com/davidteren/current_scope/issues/45)):
deterministic rule inventories for all three systems, decision report,
parity harness (per-system old-answer replay), reviewable role-backfill
migrations (enum column or rolify), and safe mechanical call-site rewrites
behind an explicit `--write`. The manual path remains the
[adoption guide](adopting-in-an-existing-app.md).

## Planned agent surfaces

These are tracked but **not shipped** — do not prompt an agent to use them
yet:

- Exposing the subject's abilities to a separate JS front-end (React/Next):
  [#96](https://github.com/davidteren/current_scope/issues/96), Inertia
  props: [#97](https://github.com/davidteren/current_scope/issues/97).
