Set up access: members, employees, mentors
Everything else builds on this. Foundations stayed the only authority on who is a member, a company employee, a visiting mentor, or a stranger — and taught Root to ask for an identity decision instead of copying membership logic into the platform. One person gets the right capabilities on every channel; everyone else gets a smaller, honest surface.
The same words produce three different results. The member's access is reset, the company employee gets the employee-scoped access tools, and the stranger is told what Foundations is and how membership works — disallowed tools are removed before planning, so the model never sees them.
- Caches the principal against the backend's immutable principal_ref and links Slack/email identities to it.
- Anchors memory, preferences, follow-ups, and history to the principal, not to an email address.
- Filters tools and layers tier-scoped prompt sections before the LLM ever plans.
- Resolves Slack ids and emails against members, company_employees, and mentors — in that order, with employees also matched by Slack profile email.
- Returns tier (member / company_employee / mentor / public), is_admin, and compact profile context; deactivated people resolve as public.
- Represents email delegates (EAs, historical CCs) as the real sender plus an acts_for mentor, never as impersonation.
How it's built — and the full prompt to build yoursExpand
How Foundations implements it
The whole contract is one endpoint plus manifest declarations. POST /integration/identity/resolve receives a channel identity and returns a principal with an immutable ref — the member or employee row id, never an email, because emails and Slack accounts change and a new ref would silently fork the person's memory. The manifest declares the tier vocabulary, gates every tool with a tiers array, and ships tier-scoped prompt_sections so the agent knows why a small surface is small — otherwise it treats member-only capabilities as malfunctions and escalates.
// Root -> Foundations
POST /integration/identity/resolve
{ "channel": "slack", "slack_user_id": "U0123ABC" }
// Foundations -> Root
{
"principal": {
"principal_ref": "3c85…immutable-member-uuid", // employees: "employee:<uuid>"
"display_name": "Priya Shah",
"email": "priya@example.com",
"tier": "member",
"is_admin": false,
"profile_context": "Founder at Example Co; Seattle member."
}
}
You are adding identity + access tiers to my community backend so it can act
as the integration behind a Root agent platform tenant (protocol root-int/1).
Root owns all conversations and calls my backend to decide who a person is.
My database stays the only authority on membership — never store membership
state on the platform side.
Context you can assume:
- I have tables for members (id uuid, name, email, slack_id, status), company
employees (id, name, email, slack_id, status, company_id), and external
mentors/office-hours hosts (id, name, email). Adapt to the actual schema.
- Root sends signed HTTP requests: x-root-signature is an HMAC-SHA256 of
"<timestamp>.<rawBody>" using our shared signing secret, x-root-timestamp
carries the timestamp. Verify both and reject stale timestamps (>5 min).
Build the following:
1. POST /integration/identity/resolve
Body: { "channel": "slack"|"email", "slack_user_id"?, "email"? }.
Resolution order for Slack: members.slack_id → company_employees.slack_id
→ company_employees by the Slack profile email. For email: members.email
→ company_employees.email → mentors.email (only when that mentor has a
current or upcoming hosted session), else tier "public".
Respond: { "principal": { principal_ref, display_name, email, tier,
is_admin, profile_context, traits } }.
- principal_ref MUST be my immutable row id ("<uuid>" for members,
"employee:<uuid>" for employees, the mentor uuid for mentors) — never an
email or name. Root anchors long-term memory to this ref; returning a
different ref for the same human silently creates a second memory.
- Tier vocabulary: member, company_employee, mentor, public. Deactivated
members resolve as public, not as their old tier.
- profile_context: 1–2 sentences (< 300 chars) the agent can use, e.g.
"Founder at Example Co; Seattle member since 2024."
2. Email delegation (executive assistants, calendar delegates, past CCs):
when the sender is not the mentor but thread/CC history shows they act
for one, return the sender as the principal (tier public) PLUS an
"acts_for" block: { reason: "current_cc"|"historical_cc"|…, principal:
<the mentor's principal> }. Root's authorization gate uses this to allow
actions on the mentor's own things while refusing anything else. Never
resolve the assistant AS the mentor.
3. GET /integration/roster — all active principals (ref, tier, email,
slack_user_id, display_name, is_admin), INCLUDING employees and mentors.
Root sweeps this to heal stale cached tiers; anyone missing from the
roster gets demoted, so forgetting employees here locks them out.
4. Manifest declarations: "access_tiers": ["member","company_employee",
"mentor","public"]; every tool gated with "tiers": [...]; and tier-scoped
"prompt_sections" that tell the agent WHO each tier is and what to do
instead of escalating (public: membership is application-based, office
hours and directories are member benefits — explain, don't apologize;
mentor: visiting host with no Slack and no member portal, building access
arrives automatically as a day-of door code).
5. Tests: the same message from a member, an employee, a mentor, and a
stranger produces four different tool surfaces; a deactivated member
resolves public; an employee whose Slack profile email differs from the
roster email still resolves by slack_id.
What to copy
- Let your system remain authoritative for identity; return a small, durable contract.
- Immutable refs. Tiers are your vocabulary. Admin status is orthogonal to tier.
- Give every limited tier a prompt section — gating without explanation reads as breakage.
Foundations: src/integration/rootPlatformAdapter.ts · resolveIdentity / rosterRoot: src/channels/identity.tsRoot: src/platform/principals.tsRoot: src/agent/emailAuthorizationGate.ts
Real features, running in production