Files

495 lines
23 KiB
Markdown
Raw Permalink Normal View History

2026-08-20 16:44:04 -04:00
# Persona Engine
A modular, configuration-driven **PowerShell 7** identity-classification service for **Microsoft Entra ID**.
The engine enumerates Entra user accounts, evaluates each one against an ordered, JSON-defined rule set, deterministically assigns **exactly one persona**, and updates a single approved persona attribute — and only when the calculated value differs from the current value.
> **Project status: Stage A implementation complete — ready for tenant validation.**
>
> 109 of 121 tasks are done. Every remaining task needs something a developer workstation does not
> have: a tenant connection (T055, T056, T101T103) or an Azure Automation account (T115T121).
>
> **Nothing has ever been run against a real directory.** The next step is [Stage A2](#stage-a2--tenant-preview-read-only)
> — a delegated, read-only `-WhatIf` run. Follow the [checklist](#tenant-validation-checklist) in order.
2026-08-20 16:44:04 -04:00
---
## Quick start
2026-08-20 16:44:04 -04:00
```bash
pwsh ./Edit-PersonaEngineConfig.ps1 -ConfigPath ./config/persona-engine.example.json -ValidateOnly -NonInteractive
```
2026-08-20 16:44:04 -04:00
That validates the configuration through all four layers. No tenant, no credentials, no network. It
is the fastest way to see what the engine does. Pass `-TestDataPath <dir>` with a directory of
synthetic user/membership fixtures to also run the real rule engine against them and see what
personas it would assign.
---
## Deployment
### Prerequisites
| Requirement | Notes |
| --- | --- |
| PowerShell 7.2 or later | Developed on 7.6.5. `pwsh -v` to check. |
| `Microsoft.Graph.Authentication` | **Runtime only.** Not needed for the config editor's offline validation. |
| PSScriptAnalyzer | Only if you want to run the lint checks locally. |
| An Entra app registration | For the persona extension property and delegated scopes. |
2026-08-24 11:31:52 -04:00
```powershell
Install-Module Microsoft.Graph.Authentication -Scope CurrentUser
Install-Module PSScriptAnalyzer -Scope CurrentUser
2026-08-20 16:44:04 -04:00
```
> Only `Microsoft.Graph.Authentication` is a runtime dependency (OTD-004). The engine calls Graph
> through `Invoke-MgGraphRequest` rather than resource-specific SDK modules, which keeps the import
> surface to one module and makes request bodies explicit values that tests can assert on — that is
> what makes the single-attribute guarantee provable.
### Step 1 — Get the code onto the target machine
```bash
git clone <REPO-URL> persona-engine
cd persona-engine
2026-08-20 16:44:04 -04:00
```
### Step 2 — Prove the machine can run it, before touching a tenant
2026-08-20 16:44:04 -04:00
```bash
pwsh ./Edit-PersonaEngineConfig.ps1 -ConfigPath ./config/persona-engine.example.json -ValidateOnly -NonInteractive
```
2026-08-20 16:44:04 -04:00
Exit code `0` expected. This needs no credentials and no network. If it does not pass, stop —
nothing downstream is trustworthy.
2026-08-20 16:44:04 -04:00
### Step 3 — Register the persona extension property
2026-08-20 16:44:04 -04:00
The persona is stored in a **directory (schema) extension property** on an app registration (OTD-001),
addressable in dynamic group rules as `user.extension_<appId>_<name>`.
```powershell
Connect-MgGraph -Scopes 'Application.ReadWrite.All'
$app = Get-MgApplication -Filter "displayName eq '<APP-REGISTRATION-NAME>'"
New-MgApplicationExtensionProperty -ApplicationId $app.Id -BodyParameter @{
name = '<APPROVED-PERSONA-ATTRIBUTE-NAME>'
dataType = 'String'
targetObjects = @('User')
}
```
Record the returned `name` — it is the full `extension_<32-hex-app-id>_<name>` string, and it is what
goes into `engine.targetAttribute`.
> `extensionAttribute1..15` were **rejected**: they cannot be written from the cloud on objects that
> are, or ever were, synchronized from on-premises, or on Exchange-originated objects. Custom
> security attributes were **rejected**: they are not exposed to the dynamic group engine, which
> defeats the purpose.
### Step 4 — Grant delegated scopes
Read-only, and enough for every preview run:
```powershell
Connect-MgGraph -Scopes 'User.Read.All','GroupMember.Read.All','RoleManagement.Read.Directory'
```
The engine requests only what the enabled rules need — a configuration with no role conditions never
asks for `RoleManagement.Read.Directory`.
`User.ReadWrite.All` is added only for Stage A3, and only after V-4 sign-off.
> **Never sign in with a standing privileged account.** A Global Administrator run invalidates V-3
> as evidence and removes every practical limit on what a defect could reach.
### Step 5 — Build the configuration
```bash
cp ./config/persona-engine.example.json ./config/persona-engine.json
```
Then replace every placeholder:
| Placeholder | Replace with |
2026-08-20 16:44:04 -04:00
| --- | --- |
| `extension_<EXTENSION-APP-ID>_<APPROVED-PERSONA-ATTRIBUTE-NAME>` | The full extension property name from step 3 |
| `<TEAM-NAME>`, `<CHANGE-REFERENCE>` | Ownership metadata |
| `<TIER0-ROLE-TEMPLATE-ID>` | Role **template** IDs (stable across tenants) |
| `00000000-0000-...` group IDs | Real group Object IDs |
| `<ORGANIZATION-NAME>` | Your company name as it appears in `companyName` |
`logging.path` was removed from the copy — it's optional and defaults to
`<current-directory>/logs/persona-engine-audit.ndjson`. Set it (or pass `-OutputPath`) only if you
want the audit log somewhere else.
> **`config/persona-engine.json` must never be committed.** It contains real group Object IDs and
> your tenant's attribute name. Keep it in a protected configuration store, and confirm `.gitignore`
> covers it. The sanitization gate scans untracked files too, so it will catch this — but do not rely
> on that as your only control.
### Step 6 — Validate, before connecting to anything
```bash
pwsh ./Edit-PersonaEngineConfig.ps1 -ConfigPath ./config/persona-engine.json -ValidateOnly -NonInteractive
```
Exit code `0` required. Codes: `1` findings · `2` warnings with `-TreatWarningsAsErrors` ·
`3` file unreadable · `4` schema unusable.
### Step 7 — Preview
```bash
pwsh ./Invoke-PersonaEngine.ps1 -ConfigPath ./config/persona-engine.json -UserObjectId <ACCOUNT-OBJECT-ID> -WhatIf -Verbose
```
One account first. Then the tenant:
```bash
pwsh ./Invoke-PersonaEngine.ps1 -ConfigPath ./config/persona-engine.json -WhatIf
```
`-WhatIf` is the **only** approved no-write control. `-Debug` does not imply read-only.
### Step 8 — Enforcement 🔒
**Blocked on the V-4 security sign-off** (T101). Do not run without `-WhatIf` against anything other
than purpose-created test accounts until that is recorded. See [docs/SecurityModel.md](docs/SecurityModel.md).
2026-08-20 16:44:04 -04:00
---
## Tenant validation checklist
2026-08-20 16:44:04 -04:00
Work through these in order. Each stage assumes the previous one passed. **Do not skip ahead** — the
whole point of the staging is that a failure is cheap at stage A1 and expensive at stage A3.
2026-08-20 16:44:04 -04:00
### Stage A1 — offline (no tenant, no credentials, no network)
2026-08-20 16:44:04 -04:00
Everything here runs on any machine with PowerShell 7.
2026-08-20 16:44:04 -04:00
- [ ] **Module manifest loads**
`pwsh -NoProfile -Command "Test-ModuleManifest ./PersonaEngine.psd1"`
Fails without `Microsoft.Graph.Authentication` installed. Expected on a bare machine.
2026-08-20 16:44:04 -04:00
- [ ] **Lint**
`pwsh -NoProfile -Command "Invoke-ScriptAnalyzer -Path . -Recurse -Settings ./PSScriptAnalyzerSettings.psd1"`
2026-08-20 16:44:04 -04:00
- [ ] **Example configuration passes all four layers**
`pwsh ./Edit-PersonaEngineConfig.ps1 -ConfigPath ./config/persona-engine.example.json -ValidateOnly -NonInteractive`
2026-08-20 16:44:04 -04:00
- [ ] **Your real configuration passes all four layers**
`pwsh ./Edit-PersonaEngineConfig.ps1 -ConfigPath ./config/persona-engine.json -ValidateOnly -NonInteractive`
2026-08-20 16:44:04 -04:00
- [ ] **Synthetic rule test produces the personas you expect**
`pwsh ./Edit-PersonaEngineConfig.ps1 -ConfigPath ./config/persona-engine.json -TestDataPath <fixtures-dir> -ValidateOnly -NonInteractive`
Point `-TestDataPath` at a directory of synthetic user/membership fixtures. If any of them
simulate a failed membership lookup, that account must show `EvaluationError`, not a persona.
If it shows a persona instead, stop — FR-013 is broken.
2026-08-20 16:44:04 -04:00
- [ ] **Drift check against the deployed configuration** (once one exists)
`pwsh ./Edit-PersonaEngineConfig.ps1 -ConfigPath ./config/persona-engine.json -PreviousConfigPath ./deployed/persona-engine.json -ValidateOnly -NonInteractive`
2026-08-20 16:44:04 -04:00
### Stage A2 — tenant preview, read-only
2026-08-20 16:44:04 -04:00
Requires delegated read scopes. **Sign in as a non-privileged account.**
- [ ] **Connect with read-only scopes and confirm no write scope was granted**
`Connect-MgGraph -Scopes 'User.Read.All','GroupMember.Read.All','RoleManagement.Read.Directory'`
then `(Get-MgContext).Scopes`
- [ ] **Single-user preview**
`pwsh ./Invoke-PersonaEngine.ps1 -ConfigPath ./config/persona-engine.json -UserObjectId <ACCOUNT-OBJECT-ID> -WhatIf -Verbose`
Expect: one result line, `WouldUpdate` or `Unchanged`, exit `0`.
- [ ] **Single-user preview against each origin type — V-1 read half (T056)**
A cloud-only account, a currently-synced account, and a formerly-synced account. Record in
`specs/001-persona-engine/verification/V-1.md`. This is the check that proves the OTD-001
attribute choice actually works where `extensionAttributeN` would not.
- [ ] **Full tenant preview**
`pwsh ./Invoke-PersonaEngine.ps1 -ConfigPath ./config/persona-engine.json -WhatIf`
Expect exit `0` and reconciliation `PASS` at every summary.
- [ ] **Record the V-3 evidence (T055)**
Confirm the run completed as a **non-privileged** account. A Global Administrator run
invalidates this item. Record in `specs/001-persona-engine/verification/V-3.md`.
- [ ] **Review the impact before going further**
From the final summary: how many `WouldUpdate`? Which rules fired, and which fired zero times?
Is the `EvaluationError` count near zero? A high count means the rule set is asking for data
the tenant will not reliably give it.
- [ ] **Idempotence against the real tenant**
Run the full preview twice. The counters must be identical.
2026-08-20 16:44:04 -04:00
- [ ] **Performance baseline (NFR-002)**
Record `durationMs` from the `RunComplete` record and the account count. No target exists yet;
this run is how one gets set.
2026-08-20 16:44:04 -04:00
### Stage A3 — delegated write, test accounts only 🔒
2026-08-20 16:44:04 -04:00
**Gated on V-4** — the written security sign-off on the six OTD-003 compensating controls
(T101, `specs/001-persona-engine/verification/V-4.md`).
- [ ] **V-4 sign-off recorded** — nothing below may start before this
- [ ] **Purpose-created test accounts exist**, one per origin type, and no other account is in scope
- [ ] **Write scope added**: `User.ReadWrite.All`, still as a non-privileged account
- [ ] **Preview the test accounts first**, one at a time, with `-WhatIf`
- [ ] **Enforce one test account**
`pwsh ./Invoke-PersonaEngine.ps1 -ConfigPath ./config/persona-engine.json -UserObjectId <TEST-ACCOUNT-OBJECT-ID>`
Confirm the confirmation prompt appears — `ConfirmImpact` is `High`.
- [ ] **Verify the write landed and touched nothing else**
Re-read the account and diff every property against a copy taken beforehand. Only the persona
attribute may have changed.
- [ ] **Confirm `previousValue` is on the audit record** — without it, rollback is impossible retroactively
- [ ] **Second run proposes zero changes** (SC-002, against a real directory this time)
- [ ] **V-1 write half (T102)** — one write per origin type; append to `V-1.md`
- [ ] **V-2 (T103)** — build a dynamic group on `user.extension_<appId>_<name>`, assign a Conditional
Access policy to it in **report-only** mode, and confirm it applies. This is what proves the
persona is actually useful rather than merely stored.
### Stage B — Azure Automation ⛔
Deferred; no Automation account available. T115T121. Run **T116 first** (`Test-Json` behaviour in
the Automation runtime) — it is the cheapest item most likely to surprise, and V-5a already showed
this cmdlet behaves in a way nobody would guess.
### If something fails
| Symptom | Look at |
| --- | --- |
| Exit `1` | The findings printed above it. No connection was attempted. |
| Exit `2` | Scopes, consent, whether the account can sign in. |
| Exit `3` | Graph availability. **No accounts were processed** — a partial population is never used. |
| Exit `4` | Group/role endpoint health. Nothing was changed. |
| Exit `5` | **An engine defect.** File an issue with the `EngineDefect` record. |
| Every account `Skipped` | Target attribute blank or unapproved. |
| Every account `EvaluationError` | A required data source is disabled or unreachable. |
| Every account `Unclassified` | Every rule disabled, or no rule matches. The summary distinguishes these. |
[docs/OperationsRunbook.md](docs/OperationsRunbook.md) has the full table, the kill switch, and the
rollback procedure.
---
## Spec-driven development
This project follows the **GitHub Spec Kit** workflow. Nothing is implemented before it is specified, planned, and decomposed into tasks.
```
Specify -> Plan -> Tasks -> Implement
2026-08-20 16:44:04 -04:00
```
| Stage | Artifact | State |
| --- | --- | --- |
| Baseline | `Persona-Engine-Developer-Handoff.txt` | Approved |
| Constitution | `.specify/memory/constitution.md` | Ratified v1.0.0 |
| Specify | `specs/001-persona-engine/spec.md` | Complete |
| Plan | `plan.md`, `research.md` | Complete — OTD-001…007, 010 resolved |
| Contracts | `data-model.md`, `contracts/`, `persona-engine.schema.json` | Complete |
| Tasks | `specs/001-persona-engine/tasks.md` | Complete — 121 tasks |
| Implement | `src/`, `docs/` | **109 / 121** — remainder needs a tenant or Automation |
---
## Core design principles
2026-08-20 16:44:04 -04:00
| Principle | Meaning |
2026-08-20 16:44:04 -04:00
| --- | --- |
| **Deterministic** | The same input and configuration always produce the same persona. |
| **Exactly one result** | Every evaluated account receives one persona — never zero, never several. |
| **First match wins** | Rules are ordered; evaluation stops at the first match. |
| **Idempotent** | Re-running changes nothing unless the calculated value actually changed. |
| **Configuration-driven** | Business rules live in JSON, never in PowerShell source. |
| **Fail safe** | If evaluation cannot complete reliably, the existing persona is preserved. |
| **Explainable** | Every result identifies the matched rule, run ID, UPN, and Account Object ID. |
| **Modular** | The pure rule engine has no dependency on Graph, Azure Automation, or the console. |
### The one that matters most
2026-08-20 16:44:04 -04:00
**Unknown is not false.** If a membership lookup fails, the condition is `Unknown`, not `False` — so a
`notMemberOf` condition does not become satisfied when the lookup fails.
2026-08-20 16:44:04 -04:00
Without that, a transient Graph outage would make every privileged account look like a non-member of
its Tier 0 group, and one run would quietly demote the entire administrative population.
---
## Components
2026-08-20 16:44:04 -04:00
### `Invoke-PersonaEngine.ps1`
2026-08-20 16:44:04 -04:00
| Parameter | Notes |
| --- | --- |
| `-ConfigPath <string>` | Defaults to `./config/persona-engine.json`. Validated through all four layers before any connection. |
| `-WhatIf` | **The approved no-write control.** |
| `-UserObjectId <GUID>` | Single-user execution. |
| `-OutputPath <string>` | Overrides `logging.path`. Both default to `<current-directory>/logs/persona-engine-audit.ndjson`. |
| `-CorrelationId <GUID>` | Run identifier; generated when absent. |
| `-SchemaPath <string>` | Schema override. |
| `-PreviousConfigPath <string>` | Enables the VR-003 drift checks. |
| `-Verbose` / `-Debug` | Common parameters. **Neither implies read-only.** |
2026-08-20 16:44:04 -04:00
**Exit codes**: `0` success · `1` config invalid · `2` auth · `3` enumeration · `4` data / threshold ·
`5` reconciliation · `6` unexpected.
2026-08-20 16:44:04 -04:00
### `Edit-PersonaEngineConfig.ps1`
2026-08-20 16:44:04 -04:00
| Parameter | Notes |
| --- | --- |
| `-ConfigPath <string>` | Required. |
| `-ValidateOnly` | Validate; never enter the editor. |
| `-NonInteractive` | Pipeline mode. Never prompts, never hangs. |
| `-SchemaPath` / `-OutputPath` | Schema override; Save-As target. |
| `-TreatWarningsAsErrors` | Escalate warnings (VR-005). |
| `-TestDataPath <string>` | Synthetic rule testing, no tenant. |
| `-PreviousConfigPath` / `-EnforcementEnabled` | Drift checks; raise safety severities. |
2026-08-20 16:44:04 -04:00
**Exit codes**: `0` valid · `1` errors · `2` warnings escalated · `3` file unreadable · `4` schema unusable.
Validation runs in four layers: **JSON syntax → JSON Schema → semantic → safety**, stopping at the
first that produces errors.
2026-08-20 16:44:04 -04:00
---
## Architecture
```
Invoke-PersonaEngine.ps1 thin wrapper: parameters, ShouldProcess, exit code
└── src/Engine/ Invoke-PersonaEngineRun (the run loop, testable offline)
├── Configuration Import, Test (4 layers), Resolve-TargetAttribute
├── Authentication Connect-PersonaGraphInteractive
├── DataProviders Get-PersonaUsers, GroupMembership, DirectoryRoles, retry, cache
├── Normalization ConvertTo-PersonaUserRecord, ConvertTo-PersonaMembershipRecord
├── RuleEngine Test-PersonaCondition/ConditionGroup/Rule, Resolve-UserPersona
├── Persistence Compare-PersonaValue, New-PersonaWriteBody, Set-UserPersonaAttribute
├── Presentation Write-UserPersonaResult, Write-PersonaSummary, reconciliation
└── Audit New-PersonaAuditRecord, Write-PersonaAuditRecord, Export-PersonaRunReport
2026-08-20 16:44:04 -04:00
```
The **rule engine is pure** — no Graph, no auth, no console, no filesystem, no clock. That purity is
what lets it be evaluated offline against synthetic fixtures with no tenant connection.
2026-08-20 16:44:04 -04:00
---
## Repository layout
2026-08-20 16:44:04 -04:00
```
Invoke-PersonaEngine.ps1 Edit-PersonaEngineConfig.ps1
PersonaEngine.psd1 PersonaEngine.psm1
config/ persona-engine.example.json, persona-engine.schema.json
src/ Configuration/ Authentication/ DataProviders/ Normalization/
RuleEngine/ Persistence/ Presentation/ Engine/ Audit/
docs/ Architecture.md SecurityModel.md ConfigurationReference.md
RuleAuthoringGuide.md OperationsRunbook.md BusinessRules.md Logging.md
specs/001-persona-engine/
spec.md plan.md tasks.md research.md data-model.md
quickstart.md traceability.md contracts/ verification/
2026-08-20 16:44:04 -04:00
```
---
## Security model
The short version; the full one is [docs/SecurityModel.md](docs/SecurityModel.md).
2026-08-20 16:44:04 -04:00
**Graph application permissions have no per-property write scope** (OTD-003). An identity that can
write the persona attribute can write any writable user property. The directory will not stop a
malformed request on our behalf, so six compensating controls hold that line, and each is tested:
2026-08-20 16:44:04 -04:00
1. `New-PersonaWriteBody` throws for any attribute other than the configured target.
2. The target must appear in `approvedWritableAttributes` — checked at validation **and** at write time.
3. Validation rejects any approved attribute that is not a directory extension property (`PE-SAF-002`).
4. One function builds the request body, and it returns a hashtable whose `Count` is exactly 1.
5. Tests inspect every body issued during a full enforcing run.
6. Code owners and branch policies gate changes to `src/Persistence/`.
2026-08-20 16:44:04 -04:00
**V-4 — written security sign-off on these controls — gates all enforcement.**
2026-08-20 16:44:04 -04:00
Never logged: tokens, `Authorization` headers, secrets, certificates, raw Graph responses. The
guarantee is structural: the record builder accepts only named, typed values, so there is nothing for
a secret to ride in on.
2026-08-20 16:44:04 -04:00
---
2026-08-20 16:44:04 -04:00
## Sanitization (SC-013)
2026-08-20 16:44:04 -04:00
No organization name, real domain, tenant or subscription ID, real UPN or Object ID, real group or
role identifier, environment-specific attribute name, or secret may appear in any file this
repository would commit.
2026-08-20 16:44:04 -04:00
Reserved domains (`example.com`, `.invalid`, `.test`) and the module manifest's own identity GUID are
exempt; nothing else is. See [docs/SecurityModel.md](docs/SecurityModel.md) for the full policy.
2026-08-20 16:44:04 -04:00
Placeholders: `<ORGANIZATION-NAME>` · `<PRIMARY-DOMAIN>` · `<TENANT-ID>` · `<ACCOUNT-OBJECT-ID>` ·
`<GROUP-OBJECT-ID>` · `<APPROVED-PERSONA-ATTRIBUTE-NAME>` · `<AUTOMATION-ACCOUNT-NAME>`
2026-08-20 16:44:04 -04:00
---
## Open decisions and verification
| ID | Status |
| --- | --- |
| OTD-001 persona attribute | **Resolved** — directory extension property |
| OTD-002 least-privilege permissions | **Resolved** |
| OTD-003 per-attribute write scope | **Resolved: not possible.** Six compensating controls; V-4 outstanding |
| OTD-004 Graph access approach | **Resolved**`Invoke-MgGraphRequest` |
| OTD-005 schema validation | **Resolved locally**`Test-Json`; V-5b open for Automation |
| OTD-006 log transport | **Resolved** — NDJSON via a single sink |
| OTD-007 retry policy | **Resolved** |
| OTD-008 incremental processing | Open — full enumeration only in v1 |
| OTD-009 concurrency lock | Open — deferred with Stage B |
| OTD-010 rollback | **Data captured**; tool out of scope for v1 |
| Verification | Status |
2026-08-20 16:44:04 -04:00
| --- | --- |
| V-1 read / write | Open — needs a tenant / test accounts |
| V-2 dynamic group + CA | Open |
| V-3 non-privileged preview | Open |
| **V-4 security sign-off** | **Open — gates enforcement** |
| V-5a `Test-Json` behaviour | **Closed** — [V-5a.md](specs/001-persona-engine/verification/V-5a.md) |
| V-5b `Test-Json` in Automation | Deferred |
Full requirement-to-test mapping, including the gaps: [traceability.md](specs/001-persona-engine/traceability.md).
2026-08-20 16:44:04 -04:00
---
## Documentation
2026-08-20 16:44:04 -04:00
| Document | For |
| --- | --- |
| [Architecture.md](docs/Architecture.md) | Boundaries, and why the rule engine is pure |
| [SecurityModel.md](docs/SecurityModel.md) | OTD-003, the six controls, V-4 |
| [ConfigurationReference.md](docs/ConfigurationReference.md) | Every field and every finding code |
| [RuleAuthoringGuide.md](docs/RuleAuthoringGuide.md) | User manual: every condition type and operator, worked examples, the interactive editor |
| [BusinessRules.md](docs/BusinessRules.md) | Writing and changing rules — the judgement calls |
| [OperationsRunbook.md](docs/OperationsRunbook.md) | Kill switch, rollback, incidents |
| [Logging.md](docs/Logging.md) | Record types and querying |
2026-08-20 16:44:04 -04:00
---
2026-08-20 16:44:04 -04:00
## Scope (version 1)
2026-08-20 16:44:04 -04:00
**In scope** — Entra **user objects only**; ordered first-match rules in JSON; property,
group-membership, and role conditions with nested `all`/`any`; `-WhatIf` as the no-write control;
structured audit logging; local PowerShell 7 and (deferred) Azure Automation.
2026-08-20 16:44:04 -04:00
**Out of scope** — service principals, managed identities, workload identities, agentic identities;
non-JSON configuration; delta processing; condition-level case-sensitivity; PIM-eligible role
assignments; a rollback tool.
2026-08-20 16:44:04 -04:00
### Candidate persona catalogue
2026-08-20 16:44:04 -04:00
`Guest` · `BreakGlass-Admin` · `Tier0-Admin` · `Tier1-Admin` · `Tier2-Admin` · `Restricted-User` ·
`Test-Account` · `Service-Account` · `Shared-Functional-Account` · `Meeting-Room-Device` · `Employee` ·
`Contractor` · `Student`
2026-08-20 16:44:04 -04:00
Two values are processing results, not rule outcomes:
2026-08-20 16:44:04 -04:00
- **`Unclassified`** — evaluation succeeded, no rule matched.
- **`EvaluationError`** — evaluation could not complete; the existing persona is preserved.