You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Make omadia an aggregator/projector of identity and roles, not an owner of them. omadia
is not an ERP and must not become one — but ERPs, HR systems, and IdPs (which already are
the org's systems of record for who exists and who is responsible for what) dock onto
omadia. omadia should project a live user/role view from those sources, joined on a
primary key, rather than maintain a second/third copy that has to be kept in sync. This is
the concrete expression of omadia's differentiator: it is itself a team-player in the
company's existing IT landscape, not another island.
This is already true for authentication (see "What already exists"). This issue extends
the same "project, don't own" pattern from access identity to org roles + employee
attributes, and establishes a platform-wide paradigm for how any module or agent
addresses a person.
Foundation for: omadia Conductor (#321, role-addressed steps & reporting) and the omadia Facilitator (#330, report/escalation to a user or role). Both should consume
this layer instead of owning role tables.
The paradigm (headline)
Principal = user:<id> | role:<key> is the DEFAULT addressee type everywhere in omadia.
Any omadia module or agent that targets a person as the destination of something — an
escalation target, a report/notification recipient, an approval/decision step, an assignment,
a hand-off — MUST by default accept either a specific user or a role.
Restricting a target to user-only (no role) is the exception, permitted ONLY when
it is technically or legally necessary to deal with one identified natural person
(e.g. a legally binding personal sign-off, a per-person secret, a regulatory 4-eyes record
tied to named individuals).
Such a user-only restriction MUST be explicitly justified and documented at the point of
definition — mirroring the existing multi_instance_justification precedent (a manifest
that sets multi_instance: false must say why; see specs/001-multi-orchestrator-runtime).
The default is role-capable; opting out costs a justification.
Rationale: people change roles, go on leave, leave the company. Hard-wiring a workflow to a
named person is brittle and a maintenance burden; addressing a role (late-bound to the
current holder) is the resilient default. The user-only case should be rare and deliberate.
Concretely, this paradigm governs (non-exhaustive): Conductor human-step addressees,
Conductor escalation/fallback targets, report and interim-status recipients, notifications,
assignments, and any future "send to / ask / require sign-off from " surface.
What already exists (grounded)
Capability
Status
Evidence
Auth is provider-based, pluggable, hot-swappable; allowed set via AUTH_PROVIDERS (default local,entra); kernel knows no concrete provider impl
EXISTS
middleware/src/auth/providerRegistry.ts (comment: "Entra moves to a plugin V1.x"), config.ts (AUTH_PROVIDERS)
Identity correlation channel ↔ auth ↔ KG in seed form: for Entra provider_user_id is the AAD object id, merged as aadObjectId; resolveIdentity(ChannelUserRef) → PlatformIdentity seam
TTL / cache / cleanup patterns for a projection cache
EXISTS (reuse)
platform/flowState.ts, plugins/jobScheduler.ts
Conclusion: "project, don't own" is already real for access. The gap is extending it to roles + attributes, and formalizing the Principal-everywhere paradigm.
Architecture — two pluggable source classes + a join/projection layer
The authentication source of truth: who can log in, stable identity. Entra/OIDC today;
local password as the standalone fallback. Already a registry (ProviderRegistry / ProviderCatalog).
2. Attribute / Role sources (NEW)
The source of truth for org roles + employee attributes (department, title, manager,
availability, …). Pluggable, independently configurable from the IdP:
The IdP itself — Entra groups / app-roles via token claims or MS Graph (when the org
expresses roles in Entra). Not pulled today; a clean extension at the provider seam.
A connector — Odoo HR / ERP as the roster + role SoT (hr.employee + position/department),
matched on work_email.
Both, joined — the common real case: access via Entra, business roles via HR.
3. Identity resolution / join layer (NEW)
Builds a live, derived Principal by correlating channel user ↔ IdP identity ↔ role/attribute
source(s) on a configurable primary key (email default; provider oid as the churn-proof
anchor). Extends the existing resolveIdentity seam and the aadObjectId merge. Output: a
Principal carrying current roles, attributes, and per-channel addresses (e.g. the Teams
conversation reference / AAD oid needed to actually deliver to a holder). Nobody maintains
user/role data in omadia.
4. Caching & freshness (NEW)
Resolution is conceptually live, but cached with a short TTL + on-demand refresh + change
webhooks where available (Entra change notifications / Odoo polling), so "always up to date"
does not mean hammering the source every turn. Reuse flowState/jobScheduler patterns.
5. Standalone fallback (NEW, narrow)
When omadia runs with no external identity/role source configured (fully stand-alone, like
OpenClaw / Hermes), and ONLY then, omadia maintains a minimal local user + role/membership
store. This is the single mode in which omadia owns this data. Conductor's previously-proposed
local conductor_role_assignments table (#321) is demoted to this fallback, not the
primary model.
Recommended modeling: generalize the provider registry
Model the role/attribute sources as a generalization of the existing auth-provider registry:
one registry pattern, two facets — AuthnProvider (who can log in) and Attribute/RoleSource
(who is who / who holds what). A source may implement both facets (Entra: authn + groups) or
just one (Odoo HR: attributes/roles only). This keeps one consistent, kernel-enforced,
manifest-declared seam — the same shape the codebase already uses for auth and for plugin
capabilities (permissionSurface). (Alternative considered: two separate registries — rejected
for adding a parallel structure with no clear benefit.)
Practical concerns (must be designed in)
Concern
Direction
Correlation key
Email as pragmatic default (Entra UPN/mail ↔ Odoo work_email ↔ Teams mail); provider oid as churn-proof anchor. Configurable.
Unmatched users
Explicit "unresolved principal" diagnostic + an override mapping table for edge cases (guests, shared mailboxes, in-HR-not-in-Entra, name/email churn). Never a silent mis-match — especially for report/escalation targets.
Role source choice
Entra groups (easy, often IT-centric) vs HR (real business roles). Source configurable independently of the IdP; joining both supported.
Freshness vs cost
TTL cache + on-demand refresh + change webhooks where available.
Channel reachability
The layer must yield each holder's channel address so a role-addressed delivery (e.g. Facilitator Teams DM, #330 B3) can actually reach them.
Privacy / data minimization
Project only the attributes a feature needs; respect existing privacy guards; cache with bounded TTL; honor GDPR posture (the projection is derived, not a new master copy).
How Conductor (#321) and Facilitator (#330) consume this
Conductor's RoleResolver becomes a facet of this Core resolution layer; a role:<key>
resolves through it to current holders (matched to omadia/channel-reachable users), live.
The local conductor_role_assignments table is demoted to the standalone fallback (above).
The Facilitator's report/escalation targets are Principals resolved here, including per-holder
channel addresses.
Acceptance criteria (V1)
An operator can configure (a) an IdP (Entra) and (b) a role/attribute source (Entra
groups/app-roles and/or an Odoo-HR roster), independently.
A logged-in user (via Entra) is resolved to a derived Principal carrying current roles and
attributes pulled from the configured source(s), correlated on the configured key — with no
per-user data maintained in omadia.
A role:<key> resolves to the current holder(s) live; changing the role in the source
(e.g. HR reassignment) changes who omadia targets, without any omadia-side edit.
An unmatched/unresolved user surfaces a clear diagnostic and can be reconciled via an override
mapping — never silently mis-addressed.
With no external source configured, omadia falls back to a minimal local user/role store and
still functions stand-alone.
The Principal paradigm is enforced: a target surface that is user-only must carry a documented
justification; otherwise it accepts user or role by default.
Scope / out of scope
In scope: the source-class abstraction + registry generalization, the join/projection
layer + correlation, caching, the standalone fallback, and the Principal-everywhere paradigm
(incl. the user-only justification rule).
Out of scope: building the specific connectors as products (the Odoo HR reader, MS Graph
group reader) beyond a reference implementation; the HR-side role-movement policy; any ERP
functionality in omadia Core.
Sequencing & dependencies
This layer (source abstraction + resolution + paradigm) — foundational.
A reference Attribute/Role source (Entra groups and/or Odoo HR) on top.
Summary & vision
Make omadia an aggregator/projector of identity and roles, not an owner of them. omadia
is not an ERP and must not become one — but ERPs, HR systems, and IdPs (which already are
the org's systems of record for who exists and who is responsible for what) dock onto
omadia. omadia should project a live user/role view from those sources, joined on a
primary key, rather than maintain a second/third copy that has to be kept in sync. This is
the concrete expression of omadia's differentiator: it is itself a team-player in the
company's existing IT landscape, not another island.
This is already true for authentication (see "What already exists"). This issue extends
the same "project, don't own" pattern from access identity to org roles + employee
attributes, and establishes a platform-wide paradigm for how any module or agent
addresses a person.
The paradigm (headline)
Principal =
user:<id>|role:<key>is the DEFAULT addressee type everywhere in omadia.Any omadia module or agent that targets a person as the destination of something — an
escalation target, a report/notification recipient, an approval/decision step, an assignment,
a hand-off — MUST by default accept either a specific user or a role.
it is technically or legally necessary to deal with one identified natural person
(e.g. a legally binding personal sign-off, a per-person secret, a regulatory 4-eyes record
tied to named individuals).
definition — mirroring the existing
multi_instance_justificationprecedent (a manifestthat sets
multi_instance: falsemust say why; seespecs/001-multi-orchestrator-runtime).The default is role-capable; opting out costs a justification.
named person is brittle and a maintenance burden; addressing a role (late-bound to the
current holder) is the resilient default. The
user-only case should be rare and deliberate.Concretely, this paradigm governs (non-exhaustive): Conductor human-step addressees,
Conductor escalation/fallback targets, report and interim-status recipients, notifications,
assignments, and any future "send to / ask / require sign-off from " surface.
What already exists (grounded)
AUTH_PROVIDERS(defaultlocal,entra); kernel knows no concrete provider implmiddleware/src/auth/providerRegistry.ts(comment: "Entra moves to a plugin V1.x"),config.ts(AUTH_PROVIDERS)middleware/src/auth/providers/EntraProvider.ts(kind: 'oidc', "Microsoft / Entra ID")middleware/src/auth/requireAuth.ts("IdP-managed users don't self-provision"),auth/whitelist.tsusersstoresprovider+provider_user_id(= Entraoid), insulating from email churnmiddleware/src/auth/migrations/0001_users.sqlprovider_user_idis the AAD object id, merged asaadObjectId;resolveIdentity(ChannelUserRef) → PlatformIdentityseammiddleware/src/index.ts(~2126–2151),middleware/src/channels/coreApi.ts(resolveIdentity)users.role IN ('admin'); Entra groups / app-roles are NOT pulled; no HR roster projection0001_users.sql(users_role_chk); nogroups/ms-graphintegration foundhr.employee,hr.department) reachable via the knowledge graph (candidate role/attribute source)middleware/packages/harness-verifier/src/graphLookupTool.tsplatform/flowState.ts,plugins/jobScheduler.tsConclusion: "project, don't own" is already real for access. The gap is extending it to
roles + attributes, and formalizing the Principal-everywhere paradigm.
Architecture — two pluggable source classes + a join/projection layer
1. Identity / Access providers (EXISTS — generalize)
The authentication source of truth: who can log in, stable identity. Entra/OIDC today;
local password as the standalone fallback. Already a registry (
ProviderRegistry/ProviderCatalog).2. Attribute / Role sources (NEW)
The source of truth for org roles + employee attributes (department, title, manager,
availability, …). Pluggable, independently configurable from the IdP:
expresses roles in Entra). Not pulled today; a clean extension at the provider seam.
hr.employee+ position/department),matched on
work_email.3. Identity resolution / join layer (NEW)
Builds a live, derived Principal by correlating channel user ↔ IdP identity ↔ role/attribute
source(s) on a configurable primary key (email default; provider
oidas the churn-proofanchor). Extends the existing
resolveIdentityseam and theaadObjectIdmerge. Output: aPrincipal carrying current roles, attributes, and per-channel addresses (e.g. the Teams
conversation reference / AAD oid needed to actually deliver to a holder). Nobody maintains
user/role data in omadia.
4. Caching & freshness (NEW)
Resolution is conceptually live, but cached with a short TTL + on-demand refresh + change
webhooks where available (Entra change notifications / Odoo polling), so "always up to date"
does not mean hammering the source every turn. Reuse
flowState/jobSchedulerpatterns.5. Standalone fallback (NEW, narrow)
When omadia runs with no external identity/role source configured (fully stand-alone, like
OpenClaw / Hermes), and ONLY then, omadia maintains a minimal local user + role/membership
store. This is the single mode in which omadia owns this data. Conductor's previously-proposed
local
conductor_role_assignmentstable (#321) is demoted to this fallback, not theprimary model.
Recommended modeling: generalize the provider registry
Model the role/attribute sources as a generalization of the existing auth-provider registry:
one registry pattern, two facets —
AuthnProvider(who can log in) andAttribute/RoleSource(who is who / who holds what). A source may implement both facets (Entra: authn + groups) or
just one (Odoo HR: attributes/roles only). This keeps one consistent, kernel-enforced,
manifest-declared seam — the same shape the codebase already uses for auth and for plugin
capabilities (
permissionSurface). (Alternative considered: two separate registries — rejectedfor adding a parallel structure with no clear benefit.)
Practical concerns (must be designed in)
work_email↔ Teams mail); provideroidas churn-proof anchor. Configurable.How Conductor (#321) and Facilitator (#330) consume this
RoleResolverbecomes a facet of this Core resolution layer; arole:<key>resolves through it to current holders (matched to omadia/channel-reachable users), live.
quorum(per omadia Facilitator — agent that moderates group chats to a defined outcome (+ Conductor JIT/ephemeral workflows & Core group primitives) #330 clarification). Both late-bound through this layer.
conductor_role_assignmentstable is demoted to the standalone fallback (above).channel addresses.
Acceptance criteria (V1)
groups/app-roles and/or an Odoo-HR roster), independently.
attributes pulled from the configured source(s), correlated on the configured key — with no
per-user data maintained in omadia.
role:<key>resolves to the current holder(s) live; changing the role in the source(e.g. HR reassignment) changes who omadia targets, without any omadia-side edit.
mapping — never silently mis-addressed.
still functions stand-alone.
justification; otherwise it accepts user or role by default.
Scope / out of scope
layer + correlation, caching, the standalone fallback, and the Principal-everywhere paradigm
(incl. the user-only justification rule).
group reader) beyond a reference implementation; the HR-side role-movement policy; any ERP
functionality in omadia Core.
Sequencing & dependencies
RoleResolverto this layer; demotes the local table to fallback.Drafted with Claude Code as the conception artifact for this feature; refine inline as needed.