command_tower 0.10.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (189) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +7 -3
  3. data/app/auth/command_tower/auth/auth_context.rb +10 -4
  4. data/app/controllers/command_tower/admin/application_controller.rb +27 -0
  5. data/app/controllers/command_tower/admin/audit/events_controller.rb +61 -0
  6. data/app/controllers/command_tower/admin/messaging/announcements_controller.rb +1 -8
  7. data/app/controllers/command_tower/admin/users/identities_controller.rb +77 -0
  8. data/app/controllers/command_tower/admin/users/impersonation_sessions_controller.rb +33 -0
  9. data/app/controllers/command_tower/admin/users/roles_controller.rb +40 -0
  10. data/app/controllers/command_tower/admin/users_controller.rb +46 -0
  11. data/app/controllers/command_tower/admin/workspace_controller.rb +15 -0
  12. data/app/controllers/command_tower/application_controller.rb +17 -1
  13. data/app/controllers/command_tower/auth/impersonation_session_controller.rb +27 -0
  14. data/app/controllers/command_tower/auth/logout_controller.rb +3 -1
  15. data/app/controllers/command_tower/auth/principal_capabilities_controller.rb +19 -0
  16. data/app/controllers/command_tower/me/audit_events_controller.rb +59 -0
  17. data/app/controllers/concerns/command_tower/api/application_response_renderer.rb +5 -0
  18. data/app/controllers/concerns/command_tower/auth/authentication_boundary.rb +3 -2
  19. data/app/controllers/concerns/command_tower/execution/http_boundary.rb +65 -0
  20. data/app/deserializers/command_tower/deserializers/admin/scope_parameter.rb +49 -0
  21. data/app/deserializers/command_tower/deserializers/admin/users.rb +218 -0
  22. data/app/deserializers/command_tower/deserializers/audit/events.rb +224 -0
  23. data/app/deserializers/command_tower/deserializers/clients/types.rb +27 -0
  24. data/app/errors/command_tower/errors/auth/admin_unavailable_during_impersonation_error.rb +17 -0
  25. data/app/errors/command_tower/errors/auth/default_membership_assignment_error.rb +17 -0
  26. data/app/errors/command_tower/errors/auth/impersonation_session_expired_error.rb +17 -0
  27. data/app/errors/command_tower/errors/auth/impersonation_session_missing_error.rb +21 -0
  28. data/app/errors/command_tower/errors/auth/nested_impersonation_error.rb +17 -0
  29. data/app/errors/command_tower/errors/auth/self_impersonation_error.rb +21 -0
  30. data/app/errors/command_tower/errors/continuation_exhausted_error.rb +25 -0
  31. data/app/jobs/command_tower/application_job.rb +7 -0
  32. data/app/jobs/command_tower/execution/job_boundary.rb +19 -0
  33. data/app/mailers/command_tower/application_mailer.rb +11 -1
  34. data/app/mailers/command_tower/messaging/channel_mailer.rb +1 -2
  35. data/app/models/command_tower/audit/event.rb +36 -0
  36. data/app/models/command_tower/impersonation/session.rb +51 -0
  37. data/app/serializers/command_tower/serializers/admin/users.rb +69 -0
  38. data/app/serializers/command_tower/serializers/admin/workspace/manifest_serializer.rb +58 -0
  39. data/app/serializers/command_tower/serializers/audit/events/filter_options_serializer.rb +44 -0
  40. data/app/serializers/command_tower/serializers/audit/events.rb +42 -0
  41. data/app/serializers/command_tower/serializers/auth/principal_capabilities_serializer.rb +15 -0
  42. data/app/serializers/command_tower/serializers/auth/session_response_serializer.rb +26 -2
  43. data/app/serializers/command_tower/serializers/impersonation/session_serializer.rb +19 -0
  44. data/app/services/command_tower/README.md +4 -10
  45. data/app/services/command_tower/authorize/validate.rb +7 -4
  46. data/app/services/command_tower/jwt/authenticate_user.rb +8 -2
  47. data/app/services/command_tower/jwt/authentication_outcome.rb +5 -4
  48. data/app/services/command_tower/jwt/login_create.rb +7 -4
  49. data/app/services/command_tower/messaging/accept/operation_logger.rb +3 -3
  50. data/app/services/command_tower/messaging/contract/observability/correlation.rb +3 -1
  51. data/app/services/command_tower/messaging/contract/observability/operation_logger.rb +3 -3
  52. data/app/services/command_tower/messaging/contract/observability/publisher.rb +44 -0
  53. data/app/services/command_tower/messaging/execution/operation_logger.rb +1 -1
  54. data/app/services/command_tower/messaging/handoff/operation_logger.rb +1 -1
  55. data/app/services/command_tower/messaging/inbox/operation_logger.rb +1 -1
  56. data/app/services/command_tower/messaging/planner/operation_logger.rb +1 -1
  57. data/app/services/command_tower/messaging/recipient_readiness/evaluate.rb +1 -1
  58. data/app/services/command_tower/service_base.rb +30 -43
  59. data/app/services/command_tower/service_logging.rb +2 -36
  60. data/app/services/command_tower/services/account/clear_phone.rb +8 -0
  61. data/app/services/command_tower/services/account/phone_verification/verify.rb +1 -0
  62. data/app/services/command_tower/services/account/update_phone.rb +9 -1
  63. data/app/services/command_tower/services/admin/users.rb +377 -0
  64. data/app/services/command_tower/services/admin/workspace/manifest.rb +49 -0
  65. data/app/services/command_tower/services/application_service.rb +37 -0
  66. data/app/services/command_tower/services/audit/events/filter_options.rb +75 -0
  67. data/app/services/command_tower/services/audit/events.rb +199 -0
  68. data/app/services/command_tower/services/auth/assign_default_membership_role.rb +44 -0
  69. data/app/services/command_tower/services/auth/authenticate_session.rb +33 -6
  70. data/app/services/command_tower/services/auth/email_verification/verify.rb +2 -0
  71. data/app/services/command_tower/services/auth/password_reset/reset.rb +13 -3
  72. data/app/services/command_tower/services/auth/plain_text/login.rb +9 -1
  73. data/app/services/command_tower/services/auth/principal_capabilities/project.rb +48 -0
  74. data/app/services/command_tower/services/impersonation/create.rb +29 -0
  75. data/app/services/command_tower/services/impersonation/end.rb +28 -0
  76. data/app/services/command_tower/services/impersonation/record_activity.rb +30 -0
  77. data/app/services/command_tower/services/impersonation/terminate_open_sessions.rb +51 -0
  78. data/app/services/command_tower/services/me/change_password.rb +12 -0
  79. data/app/shared_sequences/command_tower/shared_sequences/admin/users/resolve_scoped_user.rb +42 -0
  80. data/app/workflows/command_tower/transactional.rb +65 -0
  81. data/app/workflows/command_tower/workflows/admin/messaging/create_announcement_workflow.rb +11 -0
  82. data/app/workflows/command_tower/workflows/admin/scope_resolution.rb +17 -0
  83. data/app/workflows/command_tower/workflows/admin/users/error_mapping.rb +26 -0
  84. data/app/workflows/command_tower/workflows/admin/users/identity_mutation.rb +30 -0
  85. data/app/workflows/command_tower/workflows/admin/users/list_assignable_roles_workflow.rb +30 -0
  86. data/app/workflows/command_tower/workflows/admin/users/list_workflow.rb +54 -0
  87. data/app/workflows/command_tower/workflows/admin/users/set_email_validated_workflow.rb +45 -0
  88. data/app/workflows/command_tower/workflows/admin/users/show_workflow.rb +49 -0
  89. data/app/workflows/command_tower/workflows/admin/users/update_email_workflow.rb +45 -0
  90. data/app/workflows/command_tower/workflows/admin/users/update_name_workflow.rb +49 -0
  91. data/app/workflows/command_tower/workflows/admin/users/update_roles_workflow.rb +61 -0
  92. data/app/workflows/command_tower/workflows/admin/users/update_username_workflow.rb +45 -0
  93. data/app/workflows/command_tower/workflows/admin/workspace/manifest_workflow.rb +25 -0
  94. data/app/workflows/command_tower/workflows/application_workflow.rb +163 -24
  95. data/app/workflows/command_tower/workflows/audit/error_mapping.rb +24 -0
  96. data/app/workflows/command_tower/workflows/audit/events/filter_options_for_admin_workflow.rb +34 -0
  97. data/app/workflows/command_tower/workflows/audit/events/filter_options_for_user_workflow.rb +34 -0
  98. data/app/workflows/command_tower/workflows/audit/events/list_for_admin_workflow.rb +74 -0
  99. data/app/workflows/command_tower/workflows/audit/events/list_for_user_workflow.rb +45 -0
  100. data/app/workflows/command_tower/workflows/audit/events/show_for_admin_workflow.rb +54 -0
  101. data/app/workflows/command_tower/workflows/audit/events/show_for_user_workflow.rb +42 -0
  102. data/app/workflows/command_tower/workflows/auth/authenticate_request_workflow.rb +3 -2
  103. data/app/workflows/command_tower/workflows/auth/authentication_response_effects.rb +5 -1
  104. data/app/workflows/command_tower/workflows/auth/logout_workflow.rb +41 -1
  105. data/app/workflows/command_tower/workflows/auth/plain_text/login_workflow.rb +2 -0
  106. data/app/workflows/command_tower/workflows/auth/principal_capabilities/show_workflow.rb +27 -0
  107. data/app/workflows/command_tower/workflows/auth/register_workflow.rb +56 -24
  108. data/app/workflows/command_tower/workflows/auth/session/show_workflow.rb +15 -1
  109. data/app/workflows/command_tower/workflows/auth/session_error_status.rb +1 -0
  110. data/app/workflows/command_tower/workflows/impersonation/error_mapping.rb +27 -0
  111. data/app/workflows/command_tower/workflows/impersonation/start_workflow.rb +97 -0
  112. data/app/workflows/command_tower/workflows/impersonation/stop_workflow.rb +66 -0
  113. data/app/workflows/command_tower/workflows/me/update_name_workflow.rb +1 -0
  114. data/app/workflows/command_tower/workflows/profile/show_workflow.rb +1 -0
  115. data/app/workflows/command_tower/workflows/workflow_result.rb +31 -7
  116. data/config/routes.rb +20 -0
  117. data/db/migrate/20260816000001_create_command_tower_audit_events.rb +45 -0
  118. data/db/migrate/20260817000001_add_scope_columns_to_command_tower_audit_events.rb +34 -0
  119. data/db/migrate/20260817000003_create_command_tower_impersonation_sessions.rb +26 -0
  120. data/docs/admin_workspace.md +158 -0
  121. data/docs/api_reference.md +175 -14
  122. data/docs/architecture.md +5 -0
  123. data/docs/audit.md +195 -0
  124. data/docs/authentication.md +14 -0
  125. data/docs/authentication_authorization_guide.md +15 -11
  126. data/docs/authorization.md +25 -4
  127. data/docs/controllers.md +7 -1
  128. data/docs/eventing.md +179 -0
  129. data/docs/extending.md +32 -1
  130. data/docs/host_integration_guide.md +103 -12
  131. data/docs/initializing.md +2 -2
  132. data/docs/messaging_integration_guide.md +1 -1
  133. data/docs/models.md +4 -0
  134. data/docs/pagination.md +2 -2
  135. data/docs/principal_capabilities.md +89 -0
  136. data/docs/upgrades/0.10.0.md +2 -2
  137. data/docs/upgrades/0.11.0.md +92 -0
  138. data/docs/upgrades/README.md +1 -0
  139. data/lib/command_tower/admin_scope/apply_audit_scoping.rb +45 -0
  140. data/lib/command_tower/admin_scope/apply_users_narrowing.rb +21 -0
  141. data/lib/command_tower/admin_scope/manifest_projection.rb +102 -0
  142. data/lib/command_tower/admin_scope/resolve.rb +31 -0
  143. data/lib/command_tower/admin_scope/scope_context.rb +7 -0
  144. data/lib/command_tower/admin_scope/scope_option.rb +7 -0
  145. data/lib/command_tower/admin_scope.rb +20 -0
  146. data/lib/command_tower/admin_workspace.rb +16 -0
  147. data/lib/command_tower/audit/attribution.rb +90 -0
  148. data/lib/command_tower/audit/emit.rb +113 -0
  149. data/lib/command_tower/audit/masking.rb +41 -0
  150. data/lib/command_tower/audit/payload.rb +128 -0
  151. data/lib/command_tower/audit/persistence/subscriber.rb +85 -0
  152. data/lib/command_tower/audit.rb +27 -0
  153. data/lib/command_tower/authorization/assignable_roles.rb +36 -0
  154. data/lib/command_tower/authorization/default.yml +99 -5
  155. data/lib/command_tower/authorization/effective_entity_grants.rb +55 -0
  156. data/lib/command_tower/authorization/entity.rb +15 -5
  157. data/lib/command_tower/authorization/role.rb +5 -4
  158. data/lib/command_tower/authorization.rb +71 -10
  159. data/lib/command_tower/configuration/admin_scope/config.rb +104 -0
  160. data/lib/command_tower/configuration/admin_scope/tool_registration.rb +28 -0
  161. data/lib/command_tower/configuration/authorization/config.rb +6 -0
  162. data/lib/command_tower/configuration/config.rb +20 -0
  163. data/lib/command_tower/configuration/impersonation/config.rb +38 -0
  164. data/lib/command_tower/configuration/registry/admin_workspace/config.rb +199 -0
  165. data/lib/command_tower/configuration/registry/admin_workspace/tool_definition.rb +150 -0
  166. data/lib/command_tower/configuration/registry/audit/config.rb +403 -0
  167. data/lib/command_tower/configuration/registry/audit/event_definition.rb +155 -0
  168. data/lib/command_tower/configuration/registry/config.rb +34 -0
  169. data/lib/command_tower/configuration/registry/principal_capabilities/capability_definition.rb +43 -0
  170. data/lib/command_tower/configuration/registry/principal_capabilities/config.rb +136 -0
  171. data/lib/command_tower/current.rb +12 -0
  172. data/lib/command_tower/engine.rb +19 -4
  173. data/lib/command_tower/events.rb +184 -0
  174. data/lib/command_tower/execution.rb +111 -0
  175. data/lib/command_tower/impersonation/activity_declaration.rb +23 -0
  176. data/lib/command_tower/impersonation/apply_overlay.rb +88 -0
  177. data/lib/command_tower/impersonation/clear_overlay_for_audit.rb +23 -0
  178. data/lib/command_tower/impersonation/establish_identity.rb +44 -0
  179. data/lib/command_tower/install/baseline.rb +3 -0
  180. data/lib/command_tower/logging/lifecycle_declaration.rb +27 -0
  181. data/lib/command_tower/logging/projection.rb +67 -0
  182. data/lib/command_tower/logging/subscriber.rb +103 -0
  183. data/lib/command_tower/principal_capabilities.rb +15 -0
  184. data/lib/command_tower/version.rb +1 -1
  185. data/lib/command_tower.rb +12 -0
  186. data/spec/factories/impersonation_session.rb +17 -0
  187. data/spec/factories/user.rb +16 -0
  188. metadata +112 -2
  189. data/app/services/command_tower/messaging/contract/observability/structured_logger.rb +0 -56
@@ -0,0 +1,158 @@
1
+ # Admin Workspace
2
+
3
+ The Admin Workspace is CommandTower-owned **backend** navigation metadata: one configuration registry, boot-validated composition with host tools, and a RBAC-filtered runtime manifest. It does **not** proxy Audit or Messaging APIs. It does **not** ship a frontend shell (4.6.3+) or Audit Explorer UI (4.7). Do **not** use `GET /admin/workspace` as a generic UI permission probe — use [Principal capabilities](principal_capabilities.md) (`GET /auth/principal-capabilities`) for possessed projectable ids.
4
+
5
+ ## Register (configuration)
6
+
7
+ Registration is configuration (`class_composer`), the same pattern as `config.registry.audit`. There is no `CommandTower::Admin.register` plugin API.
8
+
9
+ CommandTower seeds platform tools. Hosts **add** tools. Hosts **cannot** redefine CommandTower-owned ids (`users`, `audit`, `messaging`). Duplicate ids and duplicate `route` values fail. There is no speculative `enabled` flag.
10
+
11
+ ```ruby
12
+ CommandTower.configure do |config|
13
+ config.registry.admin_workspace.tool :pickem_example do |tool|
14
+ tool.label = "Example"
15
+ tool.description = "Short launcher explanation of what this tool does."
16
+ tool.route = "/admin/example"
17
+ tool.group = :product
18
+ tool.sort_order = 300
19
+ tool.required_entity = :host_example_entity
20
+ tool.icon = "cube"
21
+ end
22
+ end
23
+ ```
24
+
25
+ Lookup: `CommandTower.config.registry.admin_workspace.fetch(:audit)`.
26
+
27
+ | Field | Rules |
28
+ |-------|--------|
29
+ | id | DSL name; `/\A[a-z][a-z0-9_]*\z/`; public contract, not a controller class |
30
+ | owner | `:command_tower` (seeded) or `:host` (default) |
31
+ | label | required non-blank string |
32
+ | description | optional plain-text launcher blurb; stripped; empty allowed; **hard max 160** characters (intentional exception vs unbounded `label` — free-form presentation prose). Soft authoring target ≤100. No HTML/Markdown. |
33
+ | route | required frontend path `/\A\/admin(\/[a-z][a-z0-9_-]*)+\z/` (navigation metadata, not an engine API path) |
34
+ | group | required segment token; **open** set |
35
+ | sort_order | required Integer |
36
+ | required_entity | required RBAC entity name; must exist in the composed graph after `Authorization.default_defined!` |
37
+ | icon | optional segment token or nil |
38
+
39
+ | `scope_required` | optional Boolean (default false); when true, tool APIs require host scope transport |
40
+ | `scope_parameter` | required when `scope_required`; query param key (e.g. `partition`) |
41
+ | `scope_label` | required when `scope_required`; presentation label for FE selector (e.g. `"Partition"`) |
42
+
43
+ Validate at `validate_definition!`: when `scope_required`, require non-empty `scope_parameter` + `scope_label`.
44
+
45
+ Hosts configure scope behavior on `CommandTower.config.admin_scope` (see [Resource scoping](#resource-scoping)). CT-owned tools may be extended via `configure_tool(:users)` / `configure_tool(:audit)` in host boot — do not redefine tool ids.
46
+
47
+ ## Resource scoping
48
+
49
+ Optional **host-defined resource scoping** narrows Admin tool data (Users, Audit, …) after RBAC. CommandTower remains **domain-blind** — it does not know League, Season, or tenant semantics.
50
+
51
+ ```ruby
52
+ CommandTower.configure do |config|
53
+ config.registry.admin_workspace.configure_tool(:users) do |tool|
54
+ tool.scope_required = true
55
+ tool.scope_parameter = "partition"
56
+ tool.scope_label = "Partition"
57
+ end
58
+
59
+ config.admin_scope.register(:users) do |registration|
60
+ registration.options = ->(principal:) { [CommandTower::AdminScope::ScopeOption.new(value: "a", label: "A")] }
61
+ registration.validate = ->(value:, principal:) { true }
62
+ registration.availability = ->(principal:) { { enabled: true, reason: nil } }
63
+ registration.narrow_users = ->(relation:, scope_value:, principal:, tool_id:) { relation }
64
+ registration.narrow_audit = ->(relation:, scope_value:, principal:, tool_id:) { relation }
65
+ registration.affected_users_in_scope = ->(scope_value:, principal:, tool_id:) { [] }
66
+ end
67
+ end
68
+ ```
69
+
70
+ | Hook | Purpose |
71
+ |------|---------|
72
+ | `options` | Eager scope choices for manifest (`scopeOptions`) |
73
+ | `availability` | `{ enabled:, reason: }` — disabled tools render non-navigable in FE |
74
+ | `validate` | Authorize requested scope value; fail closed → **403** |
75
+ | `narrow_users` | SQL narrowing for Users list/show |
76
+ | `narrow_audit` | SQL narrowing for host-scoped audit rows |
77
+ | `affected_users_in_scope` | User ids for eligible **global** audit events in scoped admin views |
78
+
79
+ **HTTP disclosure policy:** missing/malformed/unauthorized scope → **403**; authorized scope + resource absent from narrowed relation (nonexistent **or** out of scope) → **404** (indistinguishable).
80
+
81
+ Unscoped tools (`scope_required: false`, default) behave exactly as before — no scope query param, no `admin_scope` registration required.
82
+
83
+ Synthetic proof lives in `rails_app` (`FoundationProof::AdminScope`); Pick'em League adoption is Phase 7 only.
84
+
85
+ ## Seeded CommandTower tools
86
+
87
+ These routes are **frontend navigation targets**. Shared Admin shell consumes them; capability APIs remain independently authorized.
88
+
89
+ | id | label | description | route | group | sort_order | required_entity | icon |
90
+ |----|-------|-------------|-------|-------|------------|-----------------|------|
91
+ | `users` | Users | Find and inspect platform user accounts. | `/admin/users` | `operations` | 50 | `admin_users` | `users` |
92
+ | `audit` | Audit | Browse account and administrative audit history. | `/admin/audit` | `operations` | 100 | `admin_audit_events` | `history` |
93
+ | `messaging` | Messaging | Manage platform announcements and administrative messaging. | `/admin/messaging` | `messaging` | 200 | `admin_messaging_announcements` | `megaphone` |
94
+
95
+ RBAC CRUD, health, and Pick'em product tools are **not** seeded. Impersonation is a session primitive (`POST /admin/users/:id/impersonation-sessions`), not an Admin Workspace tool.
96
+
97
+ ## Boot validation
98
+
99
+ 1. Host `CommandTower.configure` registers tools (mutable).
100
+ 2. `after_initialize` — `audit.finalize!`, `admin_workspace.finalize!`, and `principal_capabilities.finalize!`, then freeze config (skipped in test). Stage 1 checks ids, ownership, and typed fields.
101
+ 3. `to_prepare` — `Authorization.default_defined!`, then `admin_workspace.validate_required_entities!` and `principal_capabilities.validate_required_entities!`. Stage 2 requires every `required_entity` to exist in `Entity.entities`. RBAC is not available at `after_initialize`.
102
+
103
+ The same db/install/doctor rake skip paths as audit apply.
104
+
105
+ ## Runtime HTTP
106
+
107
+ `GET /admin/workspace` (engine-relative; hosts that mount at `/api` use `GET /api/admin/workspace`).
108
+
109
+ - Authn + authz: entity `admin_workspace` (`WorkspaceController#show`). Hosts must **explicitly grant** that entity to operational roles. `owner` already has `entities: true`. Members without the entity receive **403**.
110
+ - One controller action → `Workflows::Admin::Workspace::ManifestWorkflow` (`retry_strategy :none`) → `Services::Admin::Workspace::Manifest` → `ManifestSerializer`.
111
+ - Include a tool when any of `user.roles` has `Role#allow_everything` **or** includes an entity whose name equals `required_entity`. Do not filter with `role == "admin"`.
112
+ - Host tools appear for `owner` and for **host-owned roles** that grant the host entity. Operational Admin roles are host policy; CommandTower does not ship an accumulating `admin` role.
113
+ - Order: `group` → `sort_order` → `id`.
114
+ - Envelope `data` (no pagination `meta`): `{ tools: [{ id, label, description, route, group, sortOrder, icon, scope?, scopeOptions?, availability? }] }`. Omit `owner`, `required_entity`, and Ruby classes. `icon` may be `null`. `description` is presentation metadata only and does not affect authorization.
115
+
116
+ When `scope.required` is true, manifest also includes:
117
+
118
+ | Field | Shape |
119
+ |-------|--------|
120
+ | `scope` | `{ required, parameter, label }` |
121
+ | `scopeOptions` | `[{ value, label }]` when `availability.enabled` |
122
+ | `availability` | `{ enabled, reason }` — `reason` when disabled |
123
+
124
+ Scope options are **eager** per principal on manifest fetch (no lazy scope endpoint in 5.4).
125
+
126
+ Capability APIs (`GET /admin/users`, `GET /admin/audit-events`, `POST /admin/messaging/announcements`) remain independently authorized. There is no generic `POST /admin/tools/:id/run`.
127
+
128
+ While an impersonation overlay is active, `GET /admin/workspace` remains allowed and **disables every projected tool** (`availability.enabled: false`, reason `Admin tools are unavailable while impersonating a user.`). All other `/admin/*` endpoints return **418** `admin_unavailable_during_impersonation`.
129
+
130
+ ## Least privilege
131
+
132
+ CommandTower owns the Admin **entities**. Hosts compose roles such as:
133
+
134
+ ```yaml
135
+ audit_operator:
136
+ entities:
137
+ - admin_workspace
138
+ - admin_audit_events
139
+
140
+ messaging_operator:
141
+ entities:
142
+ - admin_workspace
143
+ - admin_messaging_announcements
144
+ ```
145
+
146
+ A host may deliberately define a broad `admin` role that grants many Admin entities — that is host policy, not a CommandTower default. Tool visibility follows granted entities, never role names.
147
+
148
+ ## `/me` vs the manifest vs principal capabilities
149
+
150
+ `GET /me` remains account self-service flags, `roles`, and existing capabilities. Admin tool **discovery** is **`GET /admin/workspace` only**. Frontend-safe **possession** of projectable Admin (and host) capabilities is **`GET /auth/principal-capabilities`**. Do not copy the tool list onto `/me`, and do not treat the workspace manifest as a permission probe.
151
+
152
+ ## Related
153
+
154
+ - [API reference](api_reference.md#admin-workspace)
155
+ - [Principal capabilities](principal_capabilities.md)
156
+ - [Authorization](authorization.md)
157
+ - [Audit](audit.md)
158
+ - [Messaging](messaging_integration_guide.md)
@@ -15,13 +15,16 @@ Proof for contracts lives primarily under `spec/requests/command_tower/`.
15
15
  3. [Auth endpoints](#auth-endpoints)
16
16
  4. [Me and profile](#me-and-profile)
17
17
  5. [Me Inbox](#me-inbox)
18
- 6. [Preferences](#preferences)
19
- 7. [Phone](#phone)
20
- 8. [Pushover](#pushover)
21
- 9. [Admin messaging](#admin-messaging)
22
- 10. [Non-HTTP emit APIs](#non-http-emit-apis)
23
- 11. [RBAC overview](#rbac-overview)
24
- 12. [Feature gates](#feature-gates)
18
+ 6. [Audit events](#audit-events)
19
+ 7. [Preferences](#preferences)
20
+ 8. [Phone](#phone)
21
+ 9. [Pushover](#pushover)
22
+ 10. [Admin Workspace](#admin-workspace)
23
+ 11. [Impersonation](#impersonation)
24
+ 12. [Admin messaging](#admin-messaging)
25
+ 13. [Non-HTTP emit APIs](#non-http-emit-apis)
26
+ 14. [RBAC overview](#rbac-overview)
27
+ 15. [Feature gates](#feature-gates)
25
28
 
26
29
  ---
27
30
 
@@ -128,10 +131,37 @@ Default JWT TTL is **7 days** (`config.jwt.ttl`).
128
131
  | | |
129
132
  |--|--|
130
133
  | **Auth** | `authenticate_request!` + `authorize_request!` |
131
- | **Success** | **200** — `data`: `{ user, tokenExpiresAt }` |
134
+ | **Success** | **200** — `data`: `{ user, tokenExpiresAt, impersonation? }` |
132
135
  | **Errors** | `401`; `403`; `412` `email_verification_required` when email verification gate applies |
133
136
  | **Spec** | `spec/requests/command_tower/auth/session_spec.rb` |
134
137
 
138
+ `user` is the **effective** principal (target while overlaying). When an impersonation overlay is active, `impersonation` is present:
139
+
140
+ ```json
141
+ {
142
+ "active": true,
143
+ "sessionId": "…",
144
+ "actorUserId": 1,
145
+ "actorDisplayName": "Ada Admin",
146
+ "targetUserId": 42,
147
+ "idleExpiresAt": "…",
148
+ "absoluteExpiresAt": "…"
149
+ }
150
+ ```
151
+
152
+ Omit `impersonation` when not overlaying. Clocks are ISO timestamps from the session row. Successful idle refresh may also echo `{ impersonation: { idleExpiresAt, absoluteExpiresAt } }` on that 2xx envelope **meta** only (not on generic success).
153
+
154
+ ### `DELETE /auth/impersonation-session`
155
+
156
+ | | |
157
+ |--|--|
158
+ | **Auth** | `authenticate_request!` (overlay capture: expired overlays still authenticate the administrator) |
159
+ | **Success** | **200** — `data`: `{ message: "impersonation_ended" }`; `set_token` re-issues the administrator JWT **without** `impersonation_session_id` |
160
+ | **Errors** | `401`; `422` `impersonation_session_missing` |
161
+ | **Spec** | `spec/requests/command_tower/auth/impersonation_session_spec.rb` |
162
+
163
+ Does **not** authorize `admin_impersonation` on the effective user. Shared frontend persists `X-Authorization-Reset` on native 2xx responses.
164
+
135
165
  ### `POST /auth/signup-session`
136
166
 
137
167
  | | |
@@ -150,6 +180,17 @@ Default JWT TTL is **7 days** (`config.jwt.ttl`).
150
180
  | **Success** | **200** — `data`: password / email / username / verificationCode / phoneVerificationCode policy objects (`minLength`, `maxLength`, `pattern`, …) |
151
181
  | **Spec** | `spec/requests/command_tower/auth/identity_policy_spec.rb` |
152
182
 
183
+ ### `GET /auth/principal-capabilities`
184
+
185
+ | | |
186
+ |--|--|
187
+ | **Auth** | authenticate + authorize (RBAC entity `principal_capabilities`) |
188
+ | **Success** | **200** — `data`: `{ principalCapabilities: string[] }` (unique, sorted, possessed projectable ids only) |
189
+ | **Errors** | `401`; `403`; `412` `email_verification_required` when email verification gate applies |
190
+ | **Spec** | `spec/requests/command_tower/auth/principal_capabilities_spec.rb` |
191
+
192
+ Projection is effective entity grants ∩ curated `config.registry.principal_capabilities` (never role/group names). CommandTower seeds `admin_workspace`, `admin_users`, `admin_users_update`, `admin_rbac_assignments`, `admin_audit_events`, `admin_messaging_announcements`, `admin_impersonation`, `me_audit_events`. Hosts may register additive host-owned ids. Distinct from `/me` capabilities and from `GET /admin/workspace` (tool manifest). See [Principal capabilities](principal_capabilities.md).
193
+
153
194
  ### `GET /auth/email/availability`
154
195
 
155
196
  | | |
@@ -290,7 +331,7 @@ Rotates `verifier_token` (invalidates outstanding sessions). See [change_passwor
290
331
 
291
332
  ## Me Inbox
292
333
 
293
- All inbox routes: authenticate + authorize. Host must map RBAC entities for inbox controller actions (see dummy host `rails_app/config/rbac_groups.yml`).
334
+ All inbox routes: authenticate + authorize. Host product roles must **grant** the CT-owned `me_inbox` entity (see dummy host `rails_app/config/rbac_groups.yml`).
294
335
 
295
336
  Pagination for list: query `limit` (default **50**, max **100**), `offset` (default **0**), `scope` (`inbox` \| `archived`, default `inbox`). List `meta`: `{ limit, offset, totalCount }`. See [pagination.md](pagination.md).
296
337
 
@@ -316,6 +357,25 @@ Pagination for list: query `limit` (default **50**, max **100**), `offset` (defa
316
357
 
317
358
  ---
318
359
 
360
+ ## Audit events
361
+
362
+ Authenticate + authorize. Host `member` grants CT-owned `me_audit_events`. Engine `admin` grants `admin_audit_events`. Pagination matches Inbox: `limit` (default **50**, max **100**), `offset` (default **0**); list `meta`: `{ limit, offset, totalCount }`. See [pagination.md](pagination.md) and [audit.md](audit.md#reading-audit-history).
363
+
364
+ Sensitive `changes` from/to are **backend-masked** for both surfaces. Metadata is not masked. The Me controller does not accept a target-user id.
365
+
366
+ | Method | Path | Notes |
367
+ |--------|------|--------|
368
+ | `GET` | `/me/audit-events` | Caller's `user_history` rows only; optional `eventName`, `occurredAfter`, `occurredBefore`, `subjectType` |
369
+ | `GET` | `/me/audit-events/:id` | Same Me scope; **404** out of scope |
370
+ | `GET` | `/admin/audit-events` | Full ledger (unscoped) or scoped composite when scope param present; plus admin filters |
371
+ | `GET` | `/admin/audit-events/:id` | Full ledger by id; **404** missing or out of scope |
372
+
373
+ Scoped admin audit: host-scoped rows (`scope_class: host`) matching host context **OR** eligible global rows (`scope_class: global` + registry `global_visible_in_host_scope`) for in-scope affected users. **Legacy** rows excluded. Missing/malformed/unauthorized scope → **403**.
374
+
375
+ **Specs:** `spec/requests/command_tower/me/audit_events_spec.rb`, `admin/audit/events_spec.rb`, `admin/scoping/audit_events_spec.rb`.
376
+
377
+ ---
378
+
319
379
  ## Preferences
320
380
 
321
381
  | | Show | Update |
@@ -366,6 +426,104 @@ Errors include `422` (`pushover_already_configured`, `pushover_not_configured`,
366
426
 
367
427
  ---
368
428
 
429
+ ## Admin Workspace
430
+
431
+ ### `GET /admin/workspace`
432
+
433
+ | | |
434
+ |--|--|
435
+ | **Auth** | authenticate + authorize (RBAC entity `admin_workspace`) |
436
+ | **Success** | **200** |
437
+ | **Spec** | `spec/requests/command_tower/admin/workspace_spec.rb` |
438
+
439
+ No query parameters. Envelope `data` (no pagination `meta`):
440
+
441
+ ```json
442
+ {
443
+ "tools": [
444
+ {
445
+ "id": "audit",
446
+ "label": "Audit",
447
+ "description": "Browse account and administrative audit history.",
448
+ "route": "/admin/audit",
449
+ "group": "operations",
450
+ "sortOrder": 100,
451
+ "icon": "history",
452
+ "scope": { "required": true, "parameter": "partition", "label": "Partition" },
453
+ "scopeOptions": [{ "value": "scope-a", "label": "Scope A" }],
454
+ "availability": { "enabled": true, "reason": null }
455
+ }
456
+ ]
457
+ }
458
+ ```
459
+
460
+ Unscoped hosts omit `scope`, `scopeOptions`, and `availability`.
461
+
462
+ Tools are filtered from the composed RBAC grant graph (`allow_everything` or an entity matching the tool's `required_entity`). `description` is additive presentation metadata (soft authoring ≤100 chars; registry hard max 160). CommandTower seeds `users`, `audit`, and `messaging`. Hosts add tools via `config.registry.admin_workspace.tool`. There is no generic tool execution route. `/me` does not duplicate this list. Do not use this endpoint as a UI permission probe — use [Principal capabilities](principal_capabilities.md).
463
+
464
+ Registration and boot rules: [Admin Workspace](admin_workspace.md).
465
+
466
+ ### Admin Users
467
+
468
+ Authenticate + authorize. Host grants CT-owned `admin_users` for list/show. Identity mutations require `admin_users_update` (do not fold writes into `admin_users`). Role assignment requires `admin_rbac_assignments`. Pagination matches Inbox/Audit: `limit` (default **50**, max **100**), `offset` (default **0**); list `meta`: `{ limit, offset, totalCount }`. Free-text `search` filters email / username / first_name / last_name server-side. Safe JSON allowlist only (never password digests / verifier tokens). **No** semantic `audit(...)` on list/show (read-only inspection). Mutations emit workflow-owned `admin_direct` events. See [pagination.md](pagination.md).
469
+
470
+ When the tool declares `scope_required`, pass the configured scope query param (e.g. `partition=scope-a`). Missing/malformed/unauthorized scope → **403**. Authorized scope + user absent from narrowed relation → **404** (same as nonexistent id).
471
+
472
+ | Method | Path | Notes |
473
+ |--------|------|-------|
474
+ | `GET` | `/admin/users` | Optional `search`; optional scope param when tool is scoped; ordered by `id` DESC |
475
+ | `GET` | `/admin/users/:id` | **404** when missing or out of scope |
476
+ | `PATCH` | `/admin/users/:id/name` | `{ firstName, lastName }` — both required |
477
+ | `PATCH` | `/admin/users/:id/username` | `{ username }` |
478
+ | `PATCH` | `/admin/users/:id/email` | `{ email }`; clears `emailValidated` when the address changes |
479
+ | `PATCH` | `/admin/users/:id/email-validation` | `{ emailValidated: boolean }` — does not change email |
480
+ | `GET` | `/admin/users/assignable-roles` | Host-sourced assignable catalog (excludes `owner`) |
481
+ | `PATCH` | `/admin/users/:id/roles` | `{ roles: string[] }` — replaces assignable roles; preserves `owner` |
482
+
483
+ Success **200** `{ data, meta, errors }` where mutation `data` is the same User schema as Show. Catalog `data` is `{ roles: [{ name, description }] }`. Validation **422** `data: null`. Missing `admin_users_update` or `admin_rbac_assignments` **403**. Impersonation overlay **418**.
484
+
485
+ **Specs:** `spec/requests/command_tower/admin/users_spec.rb`, `spec/requests/command_tower/admin/users/identities_spec.rb`, `spec/requests/command_tower/admin/users/roles_spec.rb`, `spec/requests/command_tower/admin/scoping/users_spec.rb`.
486
+
487
+ Impersonation start is a separate session primitive (below), not a User mutation.
488
+
489
+ ---
490
+
491
+ ## Impersonation
492
+
493
+ Impersonation is a **server-authoritative session overlay** on the administrator JWT. `user_id` in the JWT is always the actor. Optional claim `impersonation_session_id` locates `command_tower_impersonation_sessions`. Expiration is row-authoritative (idle + absolute). HTTP activity alone does **not** refresh idle.
494
+
495
+ `config.impersonation.idle_timeout` default **10 minutes**; `absolute_timeout` default **1 hour**; idle must be less than absolute.
496
+
497
+ Qualifying workflows declare activity:
498
+
499
+ ```ruby
500
+ class SomeWorkflow < ApplicationWorkflow
501
+ retry_strategy :none
502
+ impersonation_activity!
503
+ end
504
+ ```
505
+
506
+ Successful `WorkflowResult` sets a request flag; `HttpBoundary` records one idle refresh after a 2xx response. Do not declare on AuthenticateRequest, Session show, principal-capabilities, workspace manifest, or Logout. 5.5 exemplars: `Profile::ShowWorkflow` (GET, yes), `Me::UpdateNameWorkflow` (PATCH, yes), `Messaging::Preferences::UpdateWorkflow` (PATCH, no).
507
+
508
+ Web cookie vs native Bearer: start/stop use `response_effects[:set_token]` (`X-Authorization-Reset` + body-adjacent header; cookie when enabled).
509
+
510
+ ### `POST /admin/users/:id/impersonation-sessions`
511
+
512
+ | | |
513
+ |--|--|
514
+ | **Auth** | authenticate + authorize (`admin_impersonation`) |
515
+ | **Success** | **201** — `data`: `{ id, actorUserId, targetUserId, idleExpiresAt, absoluteExpiresAt }` |
516
+ | **Errors** | `401`; `403` (RBAC); `404` (missing / out of Users scope); `418` (overlay active); `422` self-target |
517
+ | **Spec** | `spec/requests/command_tower/admin/users/impersonation_sessions_spec.rb` |
518
+
519
+ Target lookup reuses `Services::Admin::Users::Show` with the same scope query param as Users show. Concurrent sessions are allowed. Nested start while an overlay is active is rejected at the Admin prohibition boundary (**418** `admin_unavailable_during_impersonation`); `StartWorkflow` still maps nested start to **403** `nested_impersonation_forbidden` if reached.
520
+
521
+ Admin resource endpoints other than `GET /admin/workspace` return **418** while overlaying. Workspace remains allowed and disables every tool via `availability`.
522
+
523
+ Expired product request: overlay present + invalid row → **401** `impersonation_session_expired` **without** clearing the auth cookie. Client may `DELETE /auth/impersonation-session` to return to self.
524
+
525
+ ---
526
+
369
527
  ## Admin messaging
370
528
 
371
529
  ### `POST /admin/messaging/announcements`
@@ -382,7 +540,7 @@ Errors include `422` (`pushover_already_configured`, `pushover_not_configured`,
382
540
 
383
541
  **Sync response:** `mode`, `requested`, `campaignIdentity`, `accepted`, `failed`, `skipped`, `failures: [{ userId, errorCode }]`.
384
542
 
385
- Engine admin HTTP is **announcements only**. There is no `/admin` user list, modify, role assign, or impersonate surface.
543
+ Engine admin HTTP includes workspace manifest, announcements, audit events, Users list/show, and impersonation start. There is no role-assign or User-mutation admin surface.
386
544
 
387
545
  ---
388
546
 
@@ -403,10 +561,11 @@ Details: [messaging_integration_guide.md](messaging_integration_guide.md).
403
561
 
404
562
  Engine defaults (`lib/command_tower/authorization/default.yml`):
405
563
 
406
- - Group `owner` — all entities
407
- - Group `admin` entity `admin_messaging_announcements` on `AnnouncementsController#create`
564
+ - Group `owner` — all entities (`entities: true`)
565
+ - CT-owned Admin **entities** `admin_workspace`, `admin_users`, `admin_messaging_announcements`, `admin_audit_events` (and Me/Auth entities)
566
+ - **No** CommandTower operational `admin` role — hosts grant Admin entities deliberately
408
567
 
409
- Hosts **must** supply `rbac_groups.yml` entities for Me / Auth / session surfaces (fail-closed). The dummy host file `rails_app/config/rbac_groups.yml` shows a `member` mapping pattern.
568
+ Hosts **must** supply `rbac_groups.yml` **product roles** that grant CT-owned Me / Auth / session entity names (fail-closed). Operational Admin roles are host-owned (least privilege or a deliberate broad host `admin`). The dummy host file `rails_app/config/rbac_groups.yml` shows grants-only `member` plus operator examples. Do not copy CT controller/entity definitions into the host file.
410
569
 
411
570
  Configure via `CommandTower.configure { |c| c.authorization.rbac_group_path = ... }`. Deep guide: [authentication_authorization_guide.md](authentication_authorization_guide.md). Quick start: [authorization.md](authorization.md).
412
571
 
@@ -424,7 +583,7 @@ When a gate is off, the route is **not drawn** → **404**.
424
583
  | Email verification send/verify | `login.plain_text.email_verify?` |
425
584
  | Password reset send/validate/reset | `login.plain_text.password_reset?` |
426
585
 
427
- Always drawn (not route-gated): register, logout, session, signup-session, identity-policy, password-recovery-session, Me/profile/inbox/preferences/phone/pushover, admin announcements. Phone/Pushover use **503** capability errors when product adapters are unavailable.
586
+ Always drawn (not route-gated): register, logout, session, signup-session, identity-policy, principal-capabilities, password-recovery-session, Me/profile/inbox/audit-events/preferences/phone/pushover, admin workspace, admin announcements, admin audit-events. Phone/Pushover use **503** capability errors when product adapters are unavailable.
428
587
 
429
588
  ---
430
589
 
@@ -435,4 +594,6 @@ Always drawn (not route-gated): register, logout, session, signup-session, ident
435
594
  - [Sensitive changes](sensitive_routes.md)
436
595
  - [Pagination](pagination.md)
437
596
  - [Messaging](messaging_integration_guide.md)
597
+ - [Admin Workspace](admin_workspace.md)
598
+ - [Principal capabilities](principal_capabilities.md)
438
599
  - [README](../README.md)
data/docs/architecture.md CHANGED
@@ -26,5 +26,10 @@ Controller / Job → Workflow → Shared Sequences / Services → Models & Clien
26
26
  - Service capability base: [ServiceBase README](../app/services/command_tower/README.md)
27
27
  - Install and host ownership: [Initializing](initializing.md)
28
28
  - Host extension boundaries: [Extending](extending.md)
29
+ - Eventing: [Eventing](eventing.md)
30
+ - Audit authoring and durable ledger: [Audit](audit.md)
31
+ - Admin Workspace registry and manifest: [Admin Workspace](admin_workspace.md)
32
+
33
+ Architecture specs under `spec/architecture/` guard Execution Context, canonical ASN publication, kernel-owned lifecycle, logging projection, and approved `Rails.logger` exceptions.
29
34
 
30
35
  Back to [README](../README.md).
data/docs/audit.md ADDED
@@ -0,0 +1,195 @@
1
+ # Audit (authoring contract)
2
+
3
+ Explicit semantic audit is a first-class CommandTower capability on workflows and services. It is **not** inferred from lifecycle completion, ActiveRecord dirty tracking, routes, or model callbacks.
4
+
5
+ A workflow or service may emit **zero, one, or many** audit facts. **Workflows are the preferred publication boundary.** Ownership follows the **semantic fact**, not whichever layer executes SQL. Do not emit the same fact from both a workflow and a service.
6
+
7
+ Registered `command_tower.audit.*` facts persist into **one** CommandTower ledger (`command_tower_audit_events` / `CommandTower::Audit::Event`) through a **synchronous, raising** subscriber.
8
+
9
+ **Audit persistence is transaction-aware, not transaction-requiring.** `audit(...)` does not create or require a business transaction. If an ActiveRecord transaction is already active, the INSERT joins it and shares commit/rollback. If none is active, the fact persists as a standalone synchronous write. Persistence failure raises: inside a transaction that can roll back the surrounding operation; outside one, only the audit write fails loudly.
10
+
11
+ Use a transaction when the **business operation itself** requires atomicity. Emit `audit(...)` while that transaction is active when the audit fact must share the operation’s commit/rollback boundary. Otherwise `audit(...)` may persist without an enclosing transaction. An auditable workflow is not automatically a transactional workflow.
12
+
13
+ Mutation-coupled facts (`role_assigned`, `password_changed`, `phone_verified`) commonly belong inside the mutation’s transaction. Standalone facts (`session_created`, `login_failed`, `session_cleared`, `announcement_produced`, `impersonation_started`, `impersonation_ended`) need not.
14
+
15
+ Logging remains a separate consumer. Hosts do not install their own audit subscriber; the engine attaches `CommandTower::Audit::Persistence::Subscriber` on boot. Sparse hosts get the table by running `command_tower:install:migrations` then `db:migrate`.
16
+
17
+ Rows are **append-only**. Application code must not `update` or `destroy` ledger rows. Workflows and services must not `Audit::Event.create!`; only the persistence subscriber writes.
18
+
19
+ Production publishers for the catalog below **are shipped**. Query APIs read `CommandTower::Audit::Event` through List + Project services; the raw row is not the HTTP contract. Admin Workspace **backend** manifest is shipped (`GET /admin/workspace`). Shared Audit Explorer UI is shipped. Filter-options projection is shipped.
20
+
21
+ Lifecycle vs audit vs logs: [Eventing](eventing.md).
22
+
23
+ ## Register (configuration)
24
+
25
+ Registration is configuration (`class_composer`). There is no plugin `register(...)` API. CommandTower and hosts share **one** registry and **one** `command_tower.audit.*` namespace.
26
+
27
+ CommandTower-owned names are seeded by the engine. Hosts **add** names. Hosts **cannot** redefine CommandTower-owned definitions (boot/runtime fail-fast).
28
+
29
+ ```ruby
30
+ CommandTower.configure do |config|
31
+ config.registry.audit.event :wager_placed do |event|
32
+ event.enabled = true
33
+ event.user_history = true
34
+ event.label = "Wager placed"
35
+ event.tags = %w[pickem wager]
36
+ event.sensitive_fields = []
37
+ event.allowed_changes = %i[status]
38
+ event.retention = :permanent # :permanent | :ninety_days | :one_year
39
+ event.subject_required = true
40
+ event.affected_user_required = true
41
+ end
42
+ end
43
+ ```
44
+
45
+ **Policy (registration) owns:** `enabled`, `enablement_configurable`, `user_history`, `label`, `tags`, `sensitive_fields`, `allowed_changes`, `retention`, subject/affected-user required flags, **`global_visible_in_host_scope`** (default false).
46
+
47
+ **Discovery tags** (`tags`) are presentation metadata used by consumers such as Audit Explorer to help operators find event types in filter-option selectors. Tags are normalized (lowercase, stripped, unique, deterministic). They do **not** affect authorization, event publication, ledger persistence, or server-side audit filtering — there is no `?tag=` ledger filter and tags are not stored on audit rows.
48
+
49
+ **Runtime `audit(...)` owns:** occurrence name, subject, affected user, `changes`, `metadata`, explicit `attribution_mode` when required, optional **`host_context:`** (`{ type:, identifier: }`), optional **`scope_class:`** (`global` \| `host` \| `legacy`).
50
+
51
+ Lookup: `CommandTower.config.registry.audit.fetch(:wager_placed)`.
52
+
53
+ Filter-option projection (scoped catalogs for Explorer):
54
+
55
+ ```text
56
+ GET /me/audit-events/filter-options
57
+ GET /admin/audit-events/filter-options
58
+ ```
59
+
60
+ Projects `{ eventNames: [{ value, label, tags }], subjectTypes: [{ value, label }], attributionModes: […] }` from the live registry. Me includes only `user_history` events and omits attribution modes. `subjectTypes` are unique sorted registry `subject_type` values (blank omitted). Options are not authorization.
61
+
62
+ CommandTower registration defines both the canonical contract **and** whether publication enablement may be configured. Core facts are mandatory (`enablement_configurable: false`). Hosts cannot disable them. Selected noisy facts (`session_created`, `session_cleared`, `login_failed`) are enablement-configurable. Hosts may change **only** `enabled` via `CommandTower.config.registry.audit.set_enabled!(:login_failed, true)` before `finalize!`. Hosts still cannot `event :session_created`.
63
+
64
+ ```ruby
65
+ CommandTower.config.registry.audit.set_enabled!(:session_created, false)
66
+ CommandTower.config.registry.audit.set_enabled!(:login_failed, true)
67
+ ```
68
+
69
+ ## Catalog (CommandTower-owned)
70
+
71
+ | Event | Default enabled | Enablement configurable |
72
+ |-------|----------------:|------------------------:|
73
+ | `user_registered` | yes | no |
74
+ | `role_assigned` | yes | no |
75
+ | `password_changed` | yes | no |
76
+ | `email_verified` | yes | no |
77
+ | `phone_updated` | yes | no |
78
+ | `phone_cleared` | yes | no |
79
+ | `phone_verified` | yes | no |
80
+ | `announcement_produced` | yes | no |
81
+ | `impersonation_started` | yes | no |
82
+ | `impersonation_ended` | yes | no |
83
+ | `session_created` | yes | yes |
84
+ | `session_cleared` | no | yes |
85
+ | `login_failed` | no | yes |
86
+
87
+ Sensitive-field / user-history / retention stay on the **registry** for emit-time policy. At INSERT the persistence subscriber copies the **smallest historical snapshot**: `user_history`, `sensitive_fields`, and `retention`. Later registry changes must not unmask historical PII. Read APIs treat a change key as sensitive if it is in the **union** of the row snapshot and the current definition (current may be more restrictive; it cannot unmask). Account-history eligibility is **only** the snapshot `user_history` column. The snapshot does not include `enabled`, `allowed_changes`, or owner. There is no `visibility` column. Retention is stored and **not enforced** as a delete/filter yet.
88
+
89
+ Invalid names, duplicates, host overrides, and illegal policy (for example sensitive fields not in `allowed_changes`) raise `CommandTower::Audit::*` errors.
90
+
91
+ ## `audit(...)`
92
+
93
+ Available via `CommandTower::Execution::ContextAccess` on `ApplicationWorkflow` and `ServiceBase`:
94
+
95
+ ```ruby
96
+ audit(
97
+ :user_email_changed,
98
+ subject: user,
99
+ affected_user: user,
100
+ changes: { email: { from: old_email, to: new_email } },
101
+ metadata: { reason: "user_request" },
102
+ attribution_mode: nil,
103
+ subject_label: nil
104
+ )
105
+ ```
106
+
107
+ Callers do **not** build ASN strings and do **not** `Audit::Event.create!`.
108
+
109
+ Instrument name: `command_tower.audit.<registered_name>`.
110
+
111
+ ### Changes
112
+
113
+ Shape: `{ attribute: { from:, to: } }`. Empty `changes: {}` is valid (for example `password_changed`). Keys must be ⊆ registered `allowed_changes`. Extra keys **raise**; they are not dropped.
114
+
115
+ Nested hashes are published through `CommandTower::Events.publish_audit`. Generic `Events.publish` / `sanitize_payload` still keep only scalars (and arrays of scalars). The ledger column is `change_set` because ActiveRecord reserves `changes` for dirty tracking.
116
+
117
+ ### Disabled events
118
+
119
+ A registered event with `enabled = false` causes `audit(...)` to **return without emitting**. Disabled events therefore insert **no row**. Unregistered names still fail **before** persistence.
120
+
121
+ ### Attribution
122
+
123
+ | Mode | When | Actor |
124
+ |------|------|--------|
125
+ | `impersonation` | `Current.impersonation_active` | `originating_administrator_id` |
126
+ | `admin_direct` | explicit keyword | `Current.user_id` |
127
+ | `system` | explicit, or no `Current.user_id` | `nil` |
128
+ | `self_service` | default when actor id equals affected user id | `Current.user_id` |
129
+
130
+ Do not infer `admin_direct` from routes or from id inequality. `Current.user_id` remains the **effective** user under impersonation.
131
+
132
+ Lifecycle events `impersonation_started` / `impersonation_ended` record the administrator as actor and the impersonated user as affected. Product mutations executed during an overlay use `attribution_mode: impersonation` automatically from Current.
133
+
134
+ ### Fail-fast
135
+
136
+ Unregistered names, forbidden change keys, unsafe objects in `changes`/`metadata`, missing required subject/affected user, and invalid attribution **raise**. Malformed audit data is never silently discarded.
137
+
138
+ ## Transaction-aware persistence
139
+
140
+ ```text
141
+ Standalone audit
142
+ audit(...)
143
+ → INSERT
144
+
145
+ Transactional business operation
146
+ BEGIN
147
+ mutation
148
+ audit(...)
149
+ COMMIT
150
+ ```
151
+
152
+ The outer `BEGIN` exists only when the **business operation** needs atomicity. Audit does not impose it.
153
+
154
+ ## Audit publisher ownership
155
+
156
+ Workflows are the preferred owners of semantic audit facts because workflows represent canonical business orchestration. New audit publishers should be placed in the owning workflow when that can be done cleanly.
157
+
158
+ Typical shape:
159
+
160
+ ```text
161
+ orchestrate service mutation
162
+
163
+ receive meaningful result
164
+
165
+ emit semantic audit fact
166
+ ```
167
+
168
+ Services may emit audit facts when the service uniquely owns the mutation or moving publication upward would require artificial plumbing, result-contract distortion, or transaction gymnastics.
169
+
170
+ Existing service-owned audit publishers (Phase 4.3) remain supported. Consolidation toward workflow ownership is deferred platform cleanup, not a Phase 4 correctness requirement. Do not reshape service result contracts purely to satisfy audit placement without an explicit architecture decision.
171
+
172
+ Do not double-emit from workflow and service. Models, controllers, jobs, and callbacks remain inappropriate audit publishers.
173
+
174
+ ## Reading audit history
175
+
176
+ The ledger is queryable over HTTP. Controllers stay transport-only. Workflows orchestrate; they do **not** query `Audit::Event`. `Services::Audit::Events::List` owns the relation. `Services::Audit::Events::Project` duplicates hashes and masks sensitive `change_set` from/to **before** serialization. Unmasked sensitive values never leave Project toward HTTP. Both user and admin projections mask. There is no unmask permission.
177
+
178
+ | Surface | Path | RBAC entity | Scope |
179
+ |---------|------|-------------|-------|
180
+ | User | `GET /me/audit-events` | `me_audit_events` | `affected_user_id == current_user.id` **and** snapshot `user_history == true`. Actor identity never grants visibility. The Me controller does not read a target user from params. |
181
+ | Admin | `GET /admin/audit-events` | `admin_audit_events` | Full ledger when unscoped. When tool is scoped: composite host-scoped + eligible global for in-scope users; **legacy excluded**. |
182
+ | User detail | `GET /me/audit-events/:id` | `me_audit_events` | Same Me scope as list. **404** when out of scope or missing. |
183
+ | Admin detail | `GET /admin/audit-events/:id` | `admin_audit_events` | Full ledger by id when unscoped. **404** when missing or out of scope (disclosure-safe). |
184
+
185
+ **Scope provenance columns:** `scope_class` (`global` \| `host` \| `legacy`), `host_context_type`, `host_context_identifier`. Pre-scope historical rows migrated to **`legacy`** — not bulk-labeled `global`. Legacy rows are **not** eligible for scoped-admin global inclusion.
186
+
187
+ Pagination matches Inbox: `limit` default **50** (unchanged), max **100**, `offset` default 0, `meta: { limit, offset, totalCount }`. Shared Audit Explorer FE always sends `limit` from Collection pageSize (default **25**). Order is `occurred_at DESC, id DESC`.
188
+
189
+ User filters: optional `eventName` (singular alias) or `eventNames[]` (exact `action` `IN`), `occurredAfter` / `occurredBefore` (ISO8601), `subjectType` (singular alias) or `subjectTypes[]` (`IN`). Admin adds `affectedUserId`, `actorUserId`, `originatingAdministratorId`, `attributionMode`. Invalid enums/IDs are 422. Free-text / metadata JSON search is **not** supported (deferred).
190
+
191
+ Masking applies to sensitive **change keys** only (union of row `sensitive_fields` and the current registry for that action if still registered). Metadata is **not** masked: there is no metadata sensitivity grammar today. Phone uses last-four style (`*******1212`); email uses `m***@example.com`; nil stays nil; unknown/malformed values are fully redacted, never raw.
192
+
193
+ Serializer fields (camelCase), from the projection hash only: `id`, `eventName`, `eventLabel` (optional registry label; may be empty), `occurredAt`, `attributionMode`, `actor.userId`, `affectedUser.userId`, `subject.{type,id,label}`, `impersonationActive`, `originatingAdministratorId`, `changes`, `metadata`. List and show share this serializer. Snapshot internals, `event_uuid`, and execution/correlation IDs are not exposed.
194
+
195
+ Shared FE product: `@commandtower/frontend/capabilities/audit` (`AuditExplorer` scopes `admin` \| `me`). Account Activity presentation is gated by principal capability `me_audit_events`; Admin Explorer by `admin_audit_events`. Backend RBAC remains authoritative. The Admin Workspace **manifest** (`GET /admin/workspace`) discovers tools; it is not a UI permission probe.
@@ -24,6 +24,20 @@ Failure JSON may still use older `Schema::Error` shapes — not identical to the
24
24
 
25
25
  Configure secrets and JWT options during install — see [Initializing](initializing.md).
26
26
 
27
+ ## Impersonation (session overlay)
28
+
29
+ Impersonation is a **CommandTower session primitive**, not a User mutation. The administrator JWT stays the credential (`user_id` is always the actor). An optional `impersonation_session_id` claim locates a server-authoritative `command_tower_impersonation_sessions` row. Product identity (`current_user`, `Current.user_id`) becomes the target while the overlay is valid.
30
+
31
+ - Start: `POST /admin/users/:id/impersonation-sessions` (RBAC `admin_impersonation`)
32
+ - Stop / return-to-self: `DELETE /auth/impersonation-session` (valid administrator JWT; not `admin_impersonation` on the target)
33
+ - Timeouts: `config.impersonation.idle_timeout` (default 10 minutes) and `config.impersonation.absolute_timeout` (default 1 hour). Idle refresh is **workflow-declared** (`impersonation_activity!`); HTTP activity alone does not refresh idle.
34
+ - Concurrent independent sessions are allowed. Nested impersonation is forbidden.
35
+ - Expired overlay on a product request: `401` `impersonation_session_expired` without clearing the auth cookie.
36
+ - Admin resource endpoints other than `GET /admin/workspace` return **418** `admin_unavailable_during_impersonation` while overlaying. Workspace remains allowed and projects every tool `availability.enabled: false`.
37
+ - Target visibility for start is **only** Phase 5.4 Admin Resource Scoping (`ScopeResolution` + Admin Users Show). Unscoped hosts omit `adminScope`.
38
+
39
+ See [API reference](api_reference.md#impersonation) and [Authorization](authorization.md).
40
+
27
41
  ## Where to go next
28
42
 
29
43
  | Need | Guide |