stellaops-cli scope census and the operator-identity split

Verified against: commit 387a960602 (2026-09-05, SPRINT_20260722_016 AUTH-15). Re-verify:

# the ceiling, the YAML grant, the migration and the rollback agree (7 facts)
pwsh ./tools/scripts/test-targeted-xunit.ps1 \
  -Project src/Authority/StellaOps.Authority/StellaOps.Authority.Plugin.Standard.Tests/StellaOps.Authority.Plugin.Standard.Tests.csproj \
  -Class "*StellaOpsCliClientScopeCeilingTests*"
# the persisted row on a running estate is within the ceiling (reads authority.clients, not the YAML)
bash tools/scripts/deploy/postgres/apply-stellaops-cli-scope-ceiling.sh --verify

Why this document exists

stellaops-cli is the shipped human CLI client (authorization_code password refresh_token). Until 2026-09-05 it was also the estate’s manual-mint and break-glass identity: every operator script, runbook curl and QA harness that needed a human-bound token minted through it, and every grant those procedures ever needed was appended to its allowedScopes. The live authority.clients row carried 143 scopes; the shipped CLI can exercise 88. Two consequences followed:

This page records the census that decided what the CLI client keeps, where everything else went, and the two mechanisms that keep the split from silently regressing.

Method — how “the CLI needs it” was decided

A scope stays on stellaops-cli when one of three machine-checkable facts holds:

TierEvidenceCount
1src/Cli (non-test) references the StellaOpsScopes constant or the exact string literal41
2the CLI’s own product documentation (docs/modules/cli/**) names the scope+19
3a route literal in src/Cli targets an endpoint whose authorization policy requires the scope+29
OIDCopenid profile email offline_access+4

orch:quota is named in docs/modules/cli but excluded: the token endpoint requires quota_reason for it and the CLI never sends one (S001 records the same decision). Tier 1 alone is the “27 constants” figure often quoted; it under-counts because the CLI supplies most scopes through the operator’s configured Authority:Scope rather than naming them, and because a whole endpoint group can be gated by a scope the CLI never spells out (/console/* requires ui.read, so stella auth token mint needs it).

Everything else on the live row (59 scopes) leaves the CLI client. Each was checked for a consumer: operator scripts under devops/ and tools/, .gitea workflows, QA harnesses and fixtures under src/__Tests and src/Web, and runbook curl recipes. The consumers group into five operational procedures, and each procedure now has its own password-grant identity carrying exactly the union of scopes its scripts request.

Eight ceiling scopes are not granted today and were not added by this change (a narrowing change adds nothing): authority:idp.read, authority:idp.write, obs:read, ops.admin, platform.setup.admin, platform.setup.read, policy:promote, vex:ingest. The CLI names them; the corresponding verbs cannot mint through stellaops-cli until an operator adds them, which the ceiling permits.

Table A — the ceiling (93 scopes; 85 granted)

2026-09-14 (SPRINT_20260914_002 EA-5): envstate:time-anchor:write joined the ceiling and the grant. stella airgap time-anchor declare PUTs /api/v1/platform/environment-state/time-anchor/{scope} and Platform’s custody policy for that class accepts only this scope (no legacy alias, unlike airgap-seal); the CLI now requests it per class (BackendOperationsClient.ResolveEnvironmentStateWriteScope). The widening shipped as migration S060 in S058’s intersection shape — S058 is applied and checksum-pinned, so it is never edited; the guard asserts S058S060.

ScopeTierGranted to stellaops-cliEvidence
advisory-ai:operate1 (CLI source)yessrc/Cli/StellaOps.Cli/Program.cs:194
advisory-ai:view3 (CLI route to endpoint policy)yesCLI advisory-ai chat/rate-limit GETs (dominated by advisory-ai:operate, kept for least-privilege reads) -> AdvisoryAI.WebService/Security/AdvisoryAIPolicies.cs:55 (ViewPolicy)
advisory:ingest2 (CLI docs)yesdocs/modules/cli/guides/commands/auth.md:56
advisory:read1 (CLI source)yessrc/Cli/StellaOps.Cli/Program.cs:829
airgap:import1 (CLI source)yessrc/Cli/StellaOps.Cli/Services/BackendOperationsClient.cs:2214
airgap:seal1 (CLI source)yessrc/Cli/StellaOps.Cli/Services/BackendOperationsClient.cs:133
airgap:status:read3 (CLI route to endpoint policy)yesCLI airgap status (/api/offlinekit/v1) -> OfflineKit.WebService/Program.cs (OfflineKitPolicies.StatusRead)
analytics.read2 (CLI docs)yesdocs/modules/cli/guides/commands/analytics.md:6
aoc:verify1 (CLI source)yessrc/Cli/StellaOps.Cli/Program.cs:830
attest:create1 (CLI source)yessrc/Cli/StellaOps.Cli/Services/AttestorLinkClient.cs:62
attest:read1 (CLI source)yessrc/Cli/StellaOps.Cli/Services/AttestorLinkClient.cs:63
authority:idp.read1 (CLI source)no (pre-existing gap, not added)src/Cli/StellaOps.Cli/Services/BackendOperationsClient.cs:3132
authority:idp.write1 (CLI source)no (pre-existing gap, not added)src/Cli/StellaOps.Cli/Services/BackendOperationsClient.cs:3139
authority:roles.read3 (CLI route to endpoint policy)yessrc/Cli/StellaOps.Cli/Services/BackendOperationsClient.cs:6541 (GET console/admin/roles) -> Authority/Console/Admin/ConsoleAdminEndpointExtensions.cs:161
authority:roles.write3 (CLI route to endpoint policy)yesBackendOperationsClient.cs:6564,6594 (POST/PATCH console/admin/roles) -> ConsoleAdminEndpointExtensions.cs:166,173
authority:tenants.read1 (CLI source)yessrc/Cli/StellaOps.Cli/Commands/Nis2SoaCommandGroup.cs:300
authority:tenants.write3 (CLI route to endpoint policy)yessrc/Cli/StellaOps.Cli/Commands/Assurance/OperatorComplianceConfigCommandGroup.cs:338 (PUT operator-compliance) -> ConsoleAdminEndpointExtensions.cs:88-89
concelier.jobs.trigger1 (CLI source)yessrc/Cli/StellaOps.Cli/Configuration/AuthorityTokenUtilities.cs:25
concelier.merge2 (CLI docs)yesdocs/modules/cli/architecture.md:210
crypto:admin1 (CLI source)yessrc/Cli/StellaOps.Cli/Services/PlatformSecretProviderClient.cs:127
crypto:kek:read1 (CLI source)yessrc/Cli/StellaOps.Cli/Services/PlatformCryptoKekClient.cs:71
crypto:kek:rotate1 (CLI source)yessrc/Cli/StellaOps.Cli/Services/PlatformCryptoKekClient.cs:72
crypto:profile:admin1 (CLI source)yessrc/Cli/StellaOps.Cli/Services/TenantCryptoComplianceClient.cs:41
crypto:read1 (CLI source)yessrc/Cli/StellaOps.Cli/Services/PlatformCryptoKekClient.cs:73
emailOIDCyesstandard OpenID Connect scope
envstate:time-anchor:write1 (CLI source)yessrc/Cli/StellaOps.Cli/Services/BackendOperationsClient.cs (ResolveEnvironmentStateWriteScope, PUT api/v1/platform/environment-state/time-anchor/{scope}) -> Platform.WebService/Program.cs (PlatformPolicies.EnvironmentStateTimeAnchorWrite)
evidence:read1 (CLI source)yessrc/Cli/StellaOps.Cli/Services/ForensicSnapshotClient.cs:344
exceptions:approve3 (CLI route to endpoint policy)yessrc/Cli/StellaOps.Cli/Services/ExceptionClient.cs (/{id}/{transition}) -> Policy.Engine/Endpoints/Gateway/ExceptionApprovalEndpoints.cs:65-85
exceptions:read3 (CLI route to endpoint policy)yessrc/Cli/StellaOps.Cli/Services/ExceptionClient.cs (/api/policy/exceptions) -> Policy.Gateway/Endpoints/ExceptionApprovalEndpoints.cs:41
exceptions:request3 (CLI route to endpoint policy)yessrc/Cli/StellaOps.Cli/Services/ExceptionClient.cs (POST /api/policy/exceptions) -> Policy.Gateway/Endpoints/ExceptionApprovalEndpoints.cs:34
export.operator1 (CLI source)yessrc/Cli/StellaOps.Cli/Commands/AssuranceProduceClient.cs:34
export.viewer1 (CLI source)yessrc/Cli/StellaOps.Cli/Commands/AssuranceProduceClient.cs:34
findings:read1 (CLI source)yessrc/Cli/StellaOps.Cli/Services/CvssClient.cs:69
findings:write2 (CLI docs)yesdocs/modules/cli/findings-runtime.md:51
graph:read3 (CLI route to endpoint policy)yesCLI /api/graphs/ -> Graph.Api/Security/GraphPolicies.cs:36
integration:operate1 (CLI source)yessrc/Cli/StellaOps.Cli/Commands/GuardCommandGroup.cs:175
integration:read3 (CLI route to endpoint policy)yesCLI /api/v1/integrations, /api/v1/sources, /api/v1/feeds -> Integrations.WebService/Program.cs (IntegrationPolicies.Read)
integration:write3 (CLI route to endpoint policy)yesCLI /api/v1/integrations (register/update) -> Integrations.WebService/Program.cs (IntegrationPolicies.Write)
notify.operator1 (CLI source)yessrc/Cli/StellaOps.Cli/Services/Nis2IncidentClient.cs:25
notify.viewer1 (CLI source)yessrc/Cli/StellaOps.Cli/Services/Nis2IncidentClient.cs:26
obs:read1 (CLI source)no (pre-existing gap, not added)src/Cli/StellaOps.Cli/Services/ObservabilityClient.cs:177
offline_accessOIDCyesstandard OpenID Connect scope
openidOIDCyesstandard OpenID Connect scope
ops.admin2 (CLI docs)no (pre-existing gap, not added)docs/modules/cli/guides/admin/admin-reference.md:35
ops.health2 (CLI docs)yesdocs/modules/cli/guides/admin/admin-reference.md:35
orch:backfill1 (CLI source)yessrc/Cli/StellaOps.Cli/Configuration/AuthorityTokenUtilities.cs:62
orch:operate1 (CLI source)yessrc/Cli/StellaOps.Cli/Configuration/AuthorityTokenUtilities.cs:55
orch:read1 (CLI source)yessrc/Cli/StellaOps.Cli/Services/JobEngineClient.cs:394
packs.approve2 (CLI docs)yesdocs/modules/cli/guides/packs-profiles.md:47
packs.read1 (CLI source)yessrc/Cli/StellaOps.Cli/Services/PackClient.cs:74
packs.run1 (CLI source)yessrc/Cli/StellaOps.Cli/Services/PackClient.cs:136
packs.write1 (CLI source)yessrc/Cli/StellaOps.Cli/Services/PackClient.cs:314
platform.setup.admin1 (CLI source)no (pre-existing gap, not added)src/Cli/StellaOps.Cli/Services/PlatformEnvironmentSettingsClient.cs:30
platform.setup.read1 (CLI source)no (pre-existing gap, not added)src/Cli/StellaOps.Cli/Services/PlatformEnvironmentSettingsClient.cs:29
policy:activate2 (CLI docs)yesdocs/modules/cli/guides/policy.md:6
policy:audit3 (CLI route to endpoint policy)yessrc/Cli/StellaOps.Cli/Commands/Nis2SoaCommandGroup.cs:293 (api/v1/tenants/{t}/compliance-profile) -> Authority/Tenants/TenantComplianceProfileEndpointExtensions.cs:37
policy:edit2 (CLI docs)yesdocs/modules/cli/guides/policy.md:6
policy:promote2 (CLI docs)no (pre-existing gap, not added)docs/modules/cli/guides/cli-reference.md:495
policy:publish2 (CLI docs)yesdocs/modules/cli/guides/cli-reference.md:495
policy:read1 (CLI source)yessrc/Cli/StellaOps.Cli/Services/ExceptionClient.cs:70
policy:run1 (CLI source)yessrc/Cli/StellaOps.Cli/Services/CvssClient.cs:57
policy:simulate3 (CLI route to endpoint policy)yesCLI policy simulate (/api/v2/simulate, /api/vuln/simulate, /api/risk/simulate) -> Policy.Engine/Endpoints/ConsoleSimulationEndpoint.cs:19; JobEngine PolicySimulationEndpointExtensions.cs:20
profileOIDCyesstandard OpenID Connect scope
registry.admin3 (CLI route to endpoint policy)yesRegistryCommandGroup.cs:1158,1204 (PUT/DELETE /api/v1/registry/upstreams) -> ReleaseOrchestratorPolicies.cs (RegistryUpstreamAdmin)
registry:cache:admin3 (CLI route to endpoint policy)yesRegistryCommandGroup.cs:974,1022 (PUT/DELETE /api/v1/registry/cache/policies) -> ReleaseOrchestratorPolicies.cs (RegistryCacheAdmin)
registry:read3 (CLI route to endpoint policy)yessrc/Cli/StellaOps.Cli/Commands/RegistryCommandGroup.cs (GET upstreams, cache/stats) -> ReleaseOrchestratorPolicies.cs (RegistryUpstreamRead/RegistryCacheRead)
release:publish3 (CLI route to endpoint policy)yesCLI /api/v1/release-orchestrator/approvals -> ReleaseOrchestratorPolicies.cs:100 (ReleaseApprove = ReleasePublish)
release:read3 (CLI route to endpoint policy)yessrc/Cli/StellaOps.Cli/OperatorDecisionHttpClientRegistration.cs (/api/v1/release-orchestrator/*) -> ReleaseOrchestrator.WebApi/ReleaseOrchestratorPolicies.cs (ReleaseRead)
release:write3 (CLI route to endpoint policy)yesCLI release approve/reject/promote/deploy/rollback (/api/v1/release-orchestrator/operator-decisions) -> ReleaseOrchestratorPolicies.cs:94 (ReleaseWrite)
replay:read3 (CLI route to endpoint policy)yesCLI /api/v1/replay/bundles (GET only) -> Replay.WebService/ReplayAuthorizationPolicies.cs:41
sbom:read3 (CLI route to endpoint policy)yesCLI /api/v1/sboms, /api/v1/sboms/{id}/export -> SbomService/Program.cs:98 (SbomPolicies.Read)
sbom:write3 (CLI route to endpoint policy)yesCLI /api/v1/sbom/upload -> SbomService/Program.cs:1518-1520 (SbomPolicies.Write)
scanner:export2 (CLI docs)yesdocs/modules/cli/architecture.md:719
scanner:read1 (CLI source)yessrc/Cli/StellaOps.Cli/Services/ScmImportClient.cs:93
scanner:scan1 (CLI source)yessrc/Cli/StellaOps.Cli/Services/ScmImportClient.cs:69
scanner:write3 (CLI route to endpoint policy)yesCLI /api/v1/scans, /api/v1/triage, /api/v1/sources -> Scanner.WebService/Program.cs (ScansWrite/SourcesWrite/TriageWrite)
scheduler:read3 (CLI route to endpoint policy)yessrc/Cli/StellaOps.Cli/Services/JobEngineClient.cs:404 (/api/v1/jobengine) -> JobEngine.Scheduling/JobEngineJobs/JobEngineJobEndpointExtensions.cs:29 (SchedulerPolicies.JobEngineRead)
score.evaluate3 (CLI route to endpoint policy)yesCLI api/v1/score/evaluate -> Signals/Score/Endpoints/ScoreEndpoints.cs:92 (ScorePolicies.ScoreEvaluate)
score.read1 (CLI source)yessrc/Cli/StellaOps.Cli/Program.cs:803
signer:read2 (CLI docs)yesdocs/modules/cli/architecture.md:522
signer:sign1 (CLI source)yessrc/Cli/StellaOps.Cli/Commands/DoraRoiSigningPipelineFactory.cs:196
timeline:read3 (CLI route to endpoint policy)yesCLI /api/v1/timeline/events, /snapshots (GET only) -> Timeline.WebService/Program.cs (TimelinePolicies.Read)
trust:read3 (CLI route to endpoint policy)yessrc/Cli/StellaOps.Cli/Commands/WatchlistCommandGroup.cs (GET /api/v1/watchlist) -> Attestor.Application/AttestorWebServiceComposition.cs:390 (watchlist:read)
trust:write3 (CLI route to endpoint policy)yesWatchlistCommandGroup.cs (POST/PUT/DELETE /api/v1/watchlist) -> AttestorWebServiceComposition.cs:405 (watchlist:write)
ui.admin1 (CLI source)yessrc/Cli/StellaOps.Cli/Services/BackendOperationsClient.cs:3131
ui.read3 (CLI route to endpoint policy)yessrc/Cli/StellaOps.Cli/Services/AuthorityConsoleClient.cs (console/tenants, console/token/*) -> StellaOps.Authority/Console/ConsoleEndpointExtensions.cs:27 (/console group RequireStellaOpsScopes(UiRead))
vex.admin2 (CLI docs)yesdocs/modules/cli/architecture.md:722
vex:ingest2 (CLI docs)no (pre-existing gap, not added)docs/modules/cli/architecture.md:722
vex:read1 (CLI source)yessrc/Cli/StellaOps.Cli/Services/VexObservationsClient.cs:146
vuln:audit2 (CLI docs)yesdocs/modules/cli/guides/vuln-explorer-cli.md:19
vuln:investigate2 (CLI docs)yesdocs/modules/cli/guides/vuln-explorer-cli.md:17
vuln:operate2 (CLI docs)yesdocs/modules/cli/guides/vuln-explorer-cli.md:18
vuln:view1 (CLI source)yessrc/Cli/StellaOps.Cli/Services/ConcelierObservationsClient.cs:364

Table B — the 59 scopes that left stellaops-cli

“Consumers” counts the census entries (Table C) that requested the scope through stellaops-cli.

ScopeConsumersNew homeNote
authority.audit.read0none; per incident on stellaops-break-glass-operatorconsole audit read; no CLI route
authority.revocation.read0none; per incident on stellaops-break-glass-operator/revocation/status read; the CLI reads revocation exports through the bootstrap key, not this scope
authority:clients.read2stellaops-estate-admin-operator, stellaops-onboarding-operatorconsole client administration; no CLI route
authority:clients.write2stellaops-estate-admin-operator, stellaops-onboarding-operatorconsole client administration; no CLI route
authority:signing-keys.admin0none; per incident on stellaops-break-glass-operatoroperator signing administration; no CLI verb
authority:signing-keys.enroll0none; per incident on stellaops-break-glass-operatoroperator signing enrollment; no CLI verb
authority:tokens.read0stellaops-estate-admin-operatorconsole token inventory; no CLI route
authority:tokens.revoke0stellaops-estate-admin-operatorPOST /console/admin/tokens/revoke, the break-glass revoke step
authority:users.read4stellaops-estate-admin-operator, stellaops-onboarding-operatorconsole user administration; no CLI route
authority:users.write4stellaops-estate-admin-operator, stellaops-onboarding-operatorconsole user administration; no CLI route
decision-signing0none; per incident on stellaops-break-glass-operatorno CLI verb
doctor:admin0none; per incident on stellaops-break-glass-operatorno endpoint requires it (S049 retired the doctor scopes)
doctor:run0none; per incident on stellaops-break-glass-operatorno endpoint requires it (S049 retired the doctor scopes)
evidence:create5stellaops-onboarding-operator, stellaops-compliance-operator, stellaops-release-ops-operatorthe CLI only reads evidence; compliance, onboarding and release-ops windows create it
evidence:hold1stellaops-release-ops-operatorevidence legal hold (VerdictPolicies.Admin); release-ops window
export.admin0none; per incident on stellaops-break-glass-operatorprofile archive / legacy DELETE; no CLI verb
issuer-directory:admin0none; per incident on stellaops-break-glass-operatorsee issuer-directory:read
issuer-directory:read0none; per incident on stellaops-break-glass-operatorthe CLI states no issuer-directory backend is wired
issuer-directory:write0none; per incident on stellaops-break-glass-operatorsee issuer-directory:read
notify.admin0none; per incident on stellaops-break-glass-operatorchannel create/test/send/retry are notify.operator; no CLI route needs admin
notify.escalate0none; per incident on stellaops-break-glass-operatorno CLI verb
operator.verify.admin0none; per incident on stellaops-break-glass-operatorno policy registration or endpoint requires it
operator.verify.read0none; per incident on stellaops-break-glass-operatorno policy registration or endpoint requires it
orch:quota0none; per incident on stellaops-break-glass-operatorclient_credentials quota-raise scope; the CLI never sends quota_reason (S001 decision)
platform.context.read5stellaops-onboarding-operator/api/v2/context/* (Platform); the CLI never calls it. QA harnesses requested it without using it
platform.context.write2stellaops-onboarding-operator/api/v2/context/* (Platform); the CLI never calls it
platform:federation:read0none; per incident on stellaops-break-glass-operatorReleaseOrchestrator federation; the CLI federation verbs target Concelier (Concelier.Advisories.*)
platform:federation:write0none; per incident on stellaops-break-glass-operatorsee platform:federation:read
platform:sar:erase1stellaops-estate-admin-operatorsubject-access erasure (privacy administration)
platform:sar:read1stellaops-estate-admin-operatorsubject-access requests (privacy administration)
policy:approve0none; per incident on stellaops-break-glass-operatorpromotion-gate override; not a CLI route
policy:author5stellaops-compliance-operatorexception/gate authoring endpoints the CLI does not call; the compliance golden path uses it
policy:operate0none; per incident on stellaops-break-glass-operatorbudget check / console export jobs; the CLI only lists budgets (policy:read)
policy:review0none; per incident on stellaops-break-glass-operatorno serving endpoint; the CLI review verbs target routes no backend registers
registry.token.issue0none; per incident on stellaops-break-glass-operatorregistry token service (audience registry); no CLI verb
release.policy-bindings.read2stellaops-release-ops-operatorenvironment policy bindings; Invoke-Ef5SameSubjectProof.ps1 PUTs them
release.policy-bindings.write2stellaops-release-ops-operatorsee release.policy-bindings.read
release.policy.manage0none; per incident on stellaops-break-glass-operatorapproval-policy routes; no CLI verb
release.policy.read0none; per incident on stellaops-break-glass-operatorapproval-policy routes; no CLI verb
replay:write0none; per incident on stellaops-break-glass-operatorthe CLI only downloads replay bundles
router:routing:read0none; per incident on stellaops-break-glass-operatorno policy registration or endpoint requires it
router:routing:write0none; per incident on stellaops-break-glass-operatorno policy registration or endpoint requires it
sbom:attest0none; per incident on stellaops-break-glass-operatorPOST /api/v1/lineage/diff/export; the CLI export lineage verb is local
sbom:operate0none; per incident on stellaops-break-glass-operatorSbomPolicies.Internal (service-to-service)
scheduler:operate1stellaops-onboarding-operatorJobEngine run operations; the CLI JobEngine client only needs scheduler:read (JobEngineRead group)
signals:read0none; per incident on stellaops-break-glass-operatorDB-only survivor (S001), no consumer
signals:write0none; per incident on stellaops-break-glass-operatorDB-only survivor (S001), no consumer
signer:admin0none; per incident on stellaops-break-glass-operatorSigner ceremony approver; no CLI verb
signer:rotate0none; per incident on stellaops-break-glass-operatorSigner key management; no CLI verb
stellaops.admin0none; per incident on stellaops-break-glass-operatornot in the scope catalogue; S001 seed survivor
stellaops.api3none; per incident on stellaops-break-glass-operatornot in the scope catalogue; already refused invalid_scope before AUTH-15
timeline:admin0none; per incident on stellaops-break-glass-operatorno consumer
timeline:write0none; per incident on stellaops-break-glass-operatorthe CLI only reads the timeline
trust:admin0none; per incident on stellaops-break-glass-operatorno watchlist route requires it
ui.preferences.read0none; per incident on stellaops-break-glass-operatorConsole-only
ui.preferences.write0none; per incident on stellaops-break-glass-operatorConsole-only
vexhub:read1none; per incident on stellaops-break-glass-operatorno endpoint enforces it; dropped from the stella-connectors.sh password default
vexlens.read2none; per incident on stellaops-break-glass-operatorno endpoint enforces it (/api/v1/vexlens is served by no service); two Web e2e specs still request it, see Limitations
vexlens.write1none; per incident on stellaops-break-glass-operatorno endpoint enforces it; see vexlens.read

Table C — every consumer that minted through stellaops-cli, and where it mints now

ConsumerProcedureScopes requestedClient after AUTH-15Change
.gitea/workflows/auto-sbom-readiness-e2e.ymlci-e2e6stellaops-cliunchanged (all kept)
.gitea/workflows/console-layout-audit.ymlci-e2e2stellaops-cliunchanged (OIDC only)
.gitea/workflows/e2e-analyzer-coverage.ymlci-e2e6stellaops-cliunchanged (all kept)
devops/agents-targets/scripts/customer-onboarding/01b-register-inline-integrations.shonboarding9stellaops-onboarding-operatorunchanged
devops/agents-targets/scripts/customer-onboarding/_common.ps1onboarding31stellaops-onboarding-operatorunchanged scope string
devops/compose/docker-compose.cli.ymlcli-default4stellaops-clidefault STELLAOPS_AUTHORITY_SCOPE loses stellaops.api (already refused)
devops/compose/env/stella-cli.env.examplecli-default4stellaops-clidefault STELLAOPS_AUTHORITY_SCOPE loses stellaops.api (already refused)
devops/compose/scripts/compliance-golden-path.ps1compliance18stellaops-compliance-operatordefault $ClientId; unchanged scopes
devops/compose/scripts/compliance-golden-path.shcompliance19stellaops-compliance-operatordefault CLIENT_ID; unchanged scopes
devops/compose/scripts/compliance-setup.ps1compliance5stellaops-compliance-operatordefault $ClientId; unchanged scopes
devops/compose/scripts/compliance-setup.shcompliance5stellaops-compliance-operatordefault CLIENT_ID; unchanged scopes
devops/compose/scripts/sprint005-cycle003-verify.shprobe3stellaops-cliunchanged (OIDC only)
devops/compose/scripts/stella-connectors.shconnectors-password-path11stellaops-cli (opt-in password path; default isstellaops-cli-automation)vexhub:read dropped from OIDC_SCOPE_PASSWORD
docs/runbooks/general/tenant-onboarding.mdrunbook6step 1stellaops-estate-admin-operator; step 4stellaops-cli``unchanged scopes
src/Web/StellaOps.Web/e2e/subject-access-request.e2e.spec.tsqa-harness5stellaops-estate-admin-operatorunchanged scopes
src/Web/StellaOps.Web/e2e/vexlens-token-scope-forcing-function.e2e.spec.tsqa-harness3stellaops-cli (unchanged)requests vexlens.read, which no client can mint; see Limitations
src/Web/StellaOps.Web/tests/e2e/integrations/distro-vex-advisory-only-not-capped.spec.tsqa-harness6stellaops-cli (unchanged)requests vexlens.read/write and posts to /api/v1/vexlens/*, which no service serves; see Limitations
src/Web/StellaOps.Web/tests/e2e/integrations/empty-topology-live.spec.tsqa-harness8stellaops-estate-admin-operatorunchanged scopes
src/__Tests/Fixtures/Setup-TwoTenantFixture.ps1qa-harness7stellaops-estate-admin-operatorunchanged scopes
src/__Tests/playwright/_shared/auth.fixture.tsqa-harness3stellaops-clidefault scope openid profile stellaops.api -> openid profile (the old default was already refused)
tools/scripts/deploy/customer/scan-reachability.ps1qa-scan8stellaops-cliunchanged (all kept)
tools/scripts/lab/seed-packsregistry-lab-data.shlab-seed4stellaops-cliunchanged (all kept)
tools/scripts/qa/Capture-Baseline.ps1qa-scan17stellaops-cliplatform.context.read dropped (requested, never used)
tools/scripts/qa/Invoke-AdvancedAssuranceRerun.ps1probe3stellaops-cliunchanged; requests roles, which is not a scope (pre-existing, see Limitations)
tools/scripts/qa/Invoke-Ef5SameSubjectProof.ps1qa-release17stellaops-release-ops-operatorunchanged scopes (PUTs environment policy bindings)
tools/scripts/qa/Invoke-OperatorGoldenRerun.ps1qa-release20stellaops-clievidence:create evidence:hold dropped (requested, never used)
tools/scripts/qa/Invoke-OperatorScenarios.ps1qa-scan17stellaops-cliplatform.context.read dropped (requested, never used)
tools/scripts/qa/Invoke-SowLiveCaptures.ps1estate-admin5stellaops-estate-admin-operatorunchanged scopes
tools/scripts/qa/Invoke-Wave2IsolateRetest.ps1estate-admin5stellaops-estate-admin-operatorunchanged scopes
tools/scripts/qa/Provision-Ef5SourceFixture.ps1qa-release28stellaops-clirelease.policy-bindings.read/write dropped (requested, never used)
tools/scripts/qa/rar5-route-reachability-probe.pycompliance19stellaops-compliance-operatordefault CLIENT_ID; unchanged scopes
tools/scripts/qa/seed-real-scan.ps1qa-scan12stellaops-cliunchanged (all kept)
tools/scripts/qa/verify-os-analyzer-scan.ps1qa-scan7stellaops-cliunchanged (all kept)
tools/scripts/qa/verify-rar7-native-route-window.mjsprobe4stellaops-cliunchanged (all kept)
tools/scripts/run-console-four-role-live.ps1estate-admin5stellaops-estate-admin-operatorunchanged scopes

Consumers that stay on stellaops-cli do so because every scope they request is a product-CLI scope: they simulate a CLI user and should keep exercising the product path. Where a harness requested a scope it never used (platform.context.read, release.policy-bindings.*, evidence:create/hold), the request was trimmed rather than the identity widened.

Table D — the operator identities

All five follow the shape stellaops-advisory-ai-operator established: allowedGrantTypes: password (human-bound, role-gated), tenantId and tenants pinned to default (two tenants without a singular tenantId suppresses the tenant bind and advisory scopes are rejected), allowedAudiences: stellaops only (the router gateway validates aud=stellaops; per-service audiences are inert under Router__AuthorizationTrustMode=GatewayEnforced), public (the credential is the operator’s own login).

ClientScopesGrant
stellaops-estate-admin-operator20authority:clients.read authority:clients.write authority:roles.read authority:roles.write authority:tenants.read authority:tenants.write authority:tokens.read authority:tokens.revoke authority:users.read authority:users.write email evidence:read integration:read offline_access openid platform:sar:erase platform:sar:read profile ui.admin ui.read
stellaops-onboarding-operator32authority:clients.read authority:clients.write authority:tenants.read authority:tenants.write authority:users.read authority:users.write evidence:create evidence:read findings:read findings:write graph:read integration:operate integration:read integration:write offline_access openid orch:backfill orch:operate orch:read platform.context.read platform.context.write profile registry.admin release:publish release:read release:write sbom:read scanner:read scanner:write scheduler:operate scheduler:read ui.admin
stellaops-compliance-operator22authority:tenants.read authority:tenants.write evidence:create evidence:read export.operator export.viewer notify.operator notify.viewer offline_access openid orch:operate orch:read policy:author policy:read profile release:read release:write sbom:read sbom:write scanner:read scanner:scan ui.admin
stellaops-release-ops-operator23attest:read email evidence:create evidence:hold evidence:read export.operator export.viewer findings:read offline_access openid orch:operate orch:read policy:read profile release.policy-bindings.read release.policy-bindings.write release:publish release:read release:write replay:read scanner:read scanner:write vuln:operate
stellaops-break-glass-operator2openid profile

Which identity an operator reaches for:

ProcedureIdentity
tenant / user / client / role / token administration over the Authority console API; subject-access requests; console role harnessesstellaops-estate-admin-operator
customer onboarding (devops/agents-targets/scripts/customer-onboarding)stellaops-onboarding-operator
compliance golden path, operator-compliance setup, route-reachability probesstellaops-compliance-operator
release-orchestration windows: environment policy bindings, evidence holdstellaops-release-ops-operator
AdvisoryAI / OpsMemory forcing functionsstellaops-advisory-ai-operator (unchanged)
findings:recovery, and any scope no standing identity carries, appended for the duration of an incident and reset afterstellaops-break-glass-operator
anything a CLI verb doesstellaops-cli

The stella CLI itself can mint through any of them: set STELLAOPS_AUTHORITY_CLIENT_ID (or --Authority:ClientId) to the identity and request only the scopes the procedure needs.

Scope break-glass after this change

This is the scope break-glass (an elevated grant for one incident). It is distinct from the credential-recovery break-glass in docs/runbooks/authority/authority-break-glass-recovery.md.

The union-reseed asymmetry — why the database step is mandatory

StandardClientProvisioningStore.CreateOrUpdateAsync merges DB ∪ YAML for allowedScopes, audiences and tenants on every Authority boot and never shrinks a grant (Sprint 20260520_084, RESIDUAL-2). Therefore:

The narrowing is therefore a forward-only migration, S058_stellaops_cli_scope_ceiling.sql, which sorts after every append migration and intersects the persisted row with the ceiling. It converges an existing estate (on the first boot of an image that embeds it) and a fresh bootstrap (after S057) on the same set. Because embedded SQL needs an image rebuild, an estate that has not taken that image applies the identical statement by hand:

bash tools/scripts/deploy/postgres/apply-stellaops-cli-scope-ceiling.sh --apply     # snapshots the row first
bash tools/scripts/deploy/postgres/apply-stellaops-cli-scope-ceiling.sh --verify    # row ⊆ ceiling, reads the DB
bash tools/scripts/deploy/postgres/apply-stellaops-cli-scope-ceiling.sh --rollback  # the 143-scope pre-change row
bash tools/scripts/deploy/postgres/apply-stellaops-cli-scope-ceiling.sh --self-test # proves --verify detects

The script parses the ceiling out of the migration file rather than carrying its own copy, so the two cannot disagree. The rollback file (rollback-stellaops-cli-scope-ceiling.sql) is the exact pre-change row captured on 2026-09-05 — 139 YAML scopes plus four DB-only survivors (signals:read, signals:write, stellaops.admin, stellaops.api) — and RollbackScript_RestoresThePreChangeRowAndCoversEveryCurrentGrant pins that it is a superset of the current grant. Restoring the YAML alone is not a rollback: the reseed re-adds exactly what the YAML declares and nothing else.

Verify against the database, never the file. The YAML is the intent; the row is what Authority mints from.

The guard

StellaOpsCliClientScopeCeilingTests (StellaOps.Authority.Plugin.Standard.Tests) pins:

  1. the ceiling is catalogued, distinct, sorted and non-vacuous (93 entries, every one in StellaOpsScopes.All or OIDC, the 28 source constants present);
  2. stellaops-cli’s allowedScopes in standard.yaml ⊆ ceiling, and carries openid profile email offline_access and at least 50 scopes (a rewrite that drops half the grant fails too);
  3. the ARRAY[...] literal in the CURRENT ceiling migration (S060) equals the ceiling, and the prior one (S058) is a strict subset of it — a successor never drops a scope;
  4. the rollback file lists the 143-scope pre-change row plus the EA-5 scope (144) and is a superset of the current grant;
  5. the five operator identities have the D-AAI9 shape and only catalogued scopes; the break-glass identity is exactly openid profile;
  6. findings:recovery is granted to no bootstrap client at all.

Red-proof (2026-09-05): adding signer:rotate to the YAML grant fails fact 2 naming signer:rotate; deleting vuln:operate from the migration array fails fact 3 naming it. The deploy script’s --self-test proves its own detector the same way (an in-ceiling row passes, one out-of-ceiling scope fails).

Limitations recorded, not fixed