Skip to content

feat: add before_identity_resolve plugin hook#50875

Open
khshah6 wants to merge 4 commits intoopenclaw:mainfrom
khshah6:feature/identity-plugin-hook
Open

feat: add before_identity_resolve plugin hook#50875
khshah6 wants to merge 4 commits intoopenclaw:mainfrom
khshah6:feature/identity-plugin-hook

Conversation

@khshah6
Copy link
Copy Markdown

@khshah6 khshah6 commented Mar 20, 2026

Summary

  • Problem: Identity resolution for DM session scoping is hardcoded to a static session.identityLinks config map. Can't integrate with external identity services or resolve dynamically.
  • Why it matters: Deployers with existing identity infrastructure have no way to plug it in — they must manually maintain JSON mappings and hot-reload config for every user change.
  • What changed: Added before_identity_resolve plugin hook that lets plugins resolve channel-specific peer IDs (phone numbers, Slack user IDs) to canonical identities before session key construction. Integrated into Slack (DMs + slash commands), MS Teams, and WhatsApp. Added resolveInboundPeerIdentity helper in plugin-sdk for channel use. Updated docs.
  • What did NOT change (scope boundary): resolveAgentRoute stays synchronous. Group/channel peers are not affected. Slack system event routing (context.ts) is deferred — sync function with ~15 sync callers.

Change Type (select all)

  • Bug fix
  • Feature
  • Refactor
  • Docs
  • Security hardening
  • Chore/infra

Scope (select all touched areas)

  • Gateway / orchestration
  • Skills / tool execution
  • Auth / tokens
  • Memory / storage
  • Integrations
  • API / contracts
  • UI / DX
  • CI/CD / infra

Linked Issue/PR

User-visible / Behavior Changes

  • New plugin hook before_identity_resolve available for api.on() registration
  • Plugin-resolved canonical peer IDs take precedence over session.identityLinks config
  • identityLinks continues to work as fallback when no plugin is registered or plugin returns undefined
  • No config changes required — fully additive and opt-in

Security Impact (required)

  • New permissions/capabilities? No
  • Secrets/tokens handling changed? No
  • New/changed network calls? No (plugins may make network calls, but that's plugin-authored code)
  • Command/tool execution surface changed? No
  • Data access scope changed? No

Repro + Verification

Environment

  • OS: macOS (Darwin 25.3.0)
  • Runtime/container: Docker (openclaw gateway + plugin)
  • Integration/channel: WhatsApp, Slack, MS Teams

Steps

  1. Register a before_identity_resolve hook in a plugin that maps phone numbers to canonical IDs
  2. [Optional] Configure session.dmScope: "per-peer" with existing identityLinks
  3. Send a WhatsApp DM from a mapped phone number

Expected

  • Session key uses the plugin-resolved canonical ID (e.g., agent:main:direct:cann-kanchi)
  • Plugin result takes precedence over identityLinks (which would produce agent:main:direct:kanchi)
  • Group messages do not trigger the hook

Actual

  • Verified: session key is agent:main:direct:cann-kanchi (plugin-resolved) instead of agent:main:direct:kanchi (identityLinks fallback)

Evidence

  • Failing test/log before + passing after
  • Trace/log snippets
  • Screenshot/recording
  • Perf numbers (if relevant)

Test results:

  • pnpm check: pass (0 warnings, 0 errors)
  • Scoped unit tests: 74 pass (session-key, resolve-route, hooks, plugin-sdk/routing)
  • pnpm test:contracts: 125 pass
  • pnpm test:extension slack: 463 pass
  • pnpm test:extension msteams: 229 pass
  • pnpm test:extension whatsapp: 271 pass

Human Verification (required)

  • Verified scenarios: all scoped unit tests, contract tests, and extension tests for Slack/MSTeams/WhatsApp pass; pnpm check clean
  • Edge cases checked: whitespace-only canonical IDs fall through to identityLinks; email-format IDs preserved verbatim; "main" as canonical ID preserved (not discarded)
  • What you did not verify: live Slack/MS Teams E2E (WhatsApp verified locally)

Review Conversations

  • I replied to or resolved every bot review conversation I addressed in this PR.
  • I left unresolved only the conversations that still need reviewer or maintainer judgment.

Addressed P1s (normalization mangling + "main" dropped) in follow-up commit. Acknowledged P2s (hook short-circuiting, Slack system events) as follow-ups with rationale.

Compatibility / Migration

  • Backward compatible? Yes
  • Config/env changes? No
  • Migration needed? No
  • resolveAgentRoute remains synchronous — 38 callers + 12 type references unchanged
  • resolvedPeerId is an optional additive parameter — existing callers unaffected
  • Channels without hook integration continue to use identityLinks as before

Failure Recovery (if this breaks)

  • How to disable/revert this change quickly: remove the plugin's api.on("before_identity_resolve", ...) registration, or disable the plugin via plugins.entries.<id>.enabled: false
  • Files/config to restore: no config changes needed — identityLinks continues to work as fallback
  • Known bad symptoms reviewers should watch for: session key changes for existing DM users if a plugin returns different canonical IDs than identityLinks (would create new sessions instead of continuing existing ones)

Risks and Mitigations

  • Risk: Plugin-resolved canonical IDs differ from existing identityLinks canonical keys, causing session splitting for existing users
    • Mitigation: Plugin results and identityLinks keys should use the same canonical IDs. Deploy plugin alongside identityLinks first, verify session keys match, then remove identityLinks.
  • Risk: Slow or unavailable identity backend stalls inbound message handling
    • Mitigation: resolveInboundPeerIdentity wraps hook in try/catch (fail-open). Plugin authors should use AbortSignal.timeout() and local caching. Docs include caching example.

🤖 Generated with Claude Code

AI-assisted: Yes — design, implementation, and testing done with Claude Code. We understand what the code does.

@openclaw-barnacle openclaw-barnacle Bot added docs Improvements or additions to documentation channel: msteams Channel integration: msteams channel: slack Channel integration: slack channel: whatsapp-web Channel integration: whatsapp-web gateway Gateway runtime size: M labels Mar 20, 2026
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Mar 20, 2026

Greptile Summary

This PR adds a before_identity_resolve plugin hook that lets plugins dynamically resolve channel-specific peer IDs (Slack user IDs, phone numbers, etc.) to canonical identities before session key construction, supplementing the existing static session.identityLinks mechanism. The overall architecture is sound: fail-open error handling, correct cache-bypass semantics, deterministic priority ordering for multi-plugin setups, and consistent integration across Slack (DMs + slash commands), MS Teams, and WhatsApp.

Issues found:

  • Session key collision risk for email-format canonical IDsnormalizeResolvedPeerId delegates to normalizeAgentId, which replaces any character outside [a-z0-9_-] with -. This is appropriate for shell-safe agent identifiers but silently mangles the richly-formatted identifiers commonly returned by the LDAP/SSO/identity APIs the PR explicitly targets. For example, john.doe@acme.com and john-doe@acme-com both collapse to john-doe-acme-com, producing the same session key for two distinct users. A lossless encoding (or a stricter plugin contract that requires pre-sanitized slugs) is needed.
  • Reserved "main" as an invalid canonical identity — The empty-value sentinel check (normalized === DEFAULT_AGENT_ID) also silently discards the canonical peer ID "main" (case-insensitive), causing routing to fall back to identityLinks with no warning. Plugins and users have no visible indication that this string is reserved.

Confidence Score: 3/5

  • Safe to merge for alphanumeric canonical IDs (e.g. employee slugs, UUIDs); not safe for deployments that resolve email addresses or LDAP-style identifiers until the normalization issue is fixed.
  • The core hook plumbing, fail-open error handling, cache bypass, and channel integrations are all correct and well-tested. The score is reduced because the peer-ID normalization logic in normalizeResolvedPeerId reuses agent-ID sanitization rules that are semantically inappropriate for canonical user identities, creating a realistic session key collision vector for the LDAP/SSO sources explicitly described in the PR, with no tests covering non-alphanumeric canonical IDs.
  • src/routing/session-key.ts — the normalizeResolvedPeerId function needs a normalization strategy that preserves uniqueness for email-format and other richly-formatted canonical peer IDs.
Prompt To Fix All With AI
This is a comment left during a code review.
Path: src/routing/session-key.ts
Line: 118-121

Comment:
**Agent-ID normalization silently mangles email-format canonical peer IDs**

`normalizeResolvedPeerId` delegates to `normalizeAgentId`, which replaces every character outside `[a-z0-9_-]` with `-`. This is correct for agent identifiers that must be shell/path-safe, but canonical peer IDs returned by LDAP, SSO, or identity-API plugins are commonly email addresses or other richly-formatted strings.

Concrete collision scenario:
- Plugin A returns `john.doe@acme.com` → normalized to `john-doe-acme-com`
- Plugin B (different user, different domain) returns `john-doe@acme-com` → also `john-doe-acme-com`

These two distinct identities silently share the same session key.

The PR description explicitly lists "LDAP, SSO, identity APIs" as sources, yet those sources regularly return values like `john.doe@example.com`, `cn=John Doe,dc=example,dc=com`, or `john+tag@company.io` that all collapse through this transform.

The function should either:
- Reject non-alphanumeric IDs and require plugins to emit pre-sanitized slugs (with a clear doc contract and an error/warning path), or
- Use a dedicated, lossless encoding (e.g., `encodeURIComponent`-style or hex-encode the value) so no two distinct inputs can produce the same output.

```typescript
// option: encode special chars instead of collapsing them
function normalizeResolvedPeerId(value: string | undefined | null): string | null {
  const trimmed = (value ?? "").trim();
  if (!trimmed) return null;
  // Replace only characters unsafe in session key paths, preserving uniqueness
  const encoded = trimmed.toLowerCase().replace(/[^a-z0-9_@.+-]/g, "-");
  return encoded || null;
}
```

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: src/routing/session-key.ts
Line: 118-121

Comment:
**Canonical peer ID `"main"` is silently dropped**

`normalizeResolvedPeerId` returns `null` whenever `normalizeAgentId` returns `DEFAULT_AGENT_ID` (`"main"`). Because `normalizeAgentId` lowercases the input, any plugin returning the string `"main"` or `"MAIN"` as a canonical identity will have it quietly discarded, and routing will fall back to `identityLinks` with no error or log message.

While `"main"` as a canonical user identity is uncommon, this sentinel check introduces an implicit reserved word that isn't visible to plugin authors. The condition should test for an empty-after-cleaning string rather than the `DEFAULT_AGENT_ID` constant:

```suggestion
function normalizeResolvedPeerId(value: string | undefined | null): string | null {
  const trimmed = (value ?? "").trim();
  if (!trimmed) return null;
  const normalized = normalizeAgentId(trimmed);
  // Only reject if the sanitizer returned the fallback because the input was empty/all-invalid.
  // Compare against the trimmed input, not the constant, so "main" is preserved.
  return normalized === DEFAULT_AGENT_ID && trimmed.toLowerCase() !== DEFAULT_AGENT_ID
    ? null
    : normalized;
}
```

How can I resolve this? If you propose a fix, please make it concise.

Last reviewed commit: "feature: before_iden..."

Comment thread src/routing/session-key.ts
Comment thread src/routing/session-key.ts
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1a9a651567

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/routing/session-key.ts Outdated
Comment thread src/plugins/hooks.ts
Comment thread extensions/slack/src/monitor/message-handler/prepare.ts
khshah6 and others added 3 commits March 19, 2026 23:06
…edPeerId

normalizeResolvedPeerId previously delegated to normalizeAgentId, which
strips characters outside [a-z0-9_-]. This silently mangled email
addresses and other richly-formatted identifiers returned by identity
plugins (e.g., john.doe@acme.com → john-doe-acme-com), causing
potential session key collisions between distinct users.

Now applies trim + lowercase only, matching how identityLinks canonical
keys are handled. Also fixes "main" being silently discarded as a
valid canonical peer ID.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1121ac784b

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/plugins/hooks.ts
Comment thread extensions/whatsapp/src/auto-reply/monitor/on-message.ts
@khshah6 khshah6 changed the title feat(routing): add before_identity_resolve plugin hook feat: add before_identity_resolve plugin hook Mar 20, 2026
@clawsweeper
Copy link
Copy Markdown
Contributor

clawsweeper Bot commented Apr 28, 2026

Codex review: found issues before merge.

Summary
The PR adds a before_identity_resolve plugin hook, SDK helper, resolved-peer session-key plumbing for Slack/MS Teams/WhatsApp, docs, and regression tests.

Reproducibility: yes. Current main does not contain the requested hook/helper/resolved-peer surface, and the PR blockers are source-reproducible by comparing the diff with current hook-types, runModifyingHook, and secondary channel routing paths.

Next step before merge
This is a public plugin API and DM session-isolation feature, so maintainers should approve the contract and security model before any repair lane rewrites the branch.

Security
Needs attention: The diff introduces a plugin-controlled DM identity hook that can affect transcript isolation and needs an explicit trust contract before merge.

Review findings

  • [P2] Move the hook contract into hook-types — src/plugins/types.ts:1368-1396
  • [P2] Pass a modifying-hook policy object — src/plugins/hooks.ts:455-457
  • [P2] Ignore blank canonical IDs before selecting a winner — src/plugins/hooks.ts:456
Review details

Best possible solution:

Land the feature only after a maintainer-approved hook contract lives in hook-types.ts, first-valid identity semantics are enforced across all DM routing paths, and docs/changelog clearly define the session-isolation trust boundary.

Do we have a high-confidence way to reproduce the issue?

Yes. Current main does not contain the requested hook/helper/resolved-peer surface, and the PR blockers are source-reproducible by comparing the diff with current hook-types, runModifyingHook, and secondary channel routing paths.

Is this the best way to solve the issue?

No. The additive hook direction is plausible, but this branch is not the best current solution until it rebases onto the active hook contract, handles first-valid results and secondary routes, and documents the DM isolation trust model.

Full review comments:

  • [P2] Move the hook contract into hook-types — src/plugins/types.ts:1368-1396
    Current main exports hook names, validation, and handler maps from src/plugins/hook-types.ts, while types.ts only re-exports that surface. Adding before_identity_resolve only in types.ts leaves api.on() validation unaware of the new public hook after rebase.
    Confidence: 0.9
  • [P2] Pass a modifying-hook policy object — src/plugins/hooks.ts:455-457
    runModifyingHook now expects a policy object with mergeResults and optional shouldStop. Passing a bare merge function means the current runner will not apply the intended first-winner merge/stop policy and should fail type checking after rebase.
    Confidence: 0.89
  • [P2] Ignore blank canonical IDs before selecting a winner — src/plugins/hooks.ts:456
    The merge expression treats an empty or whitespace canonicalPeerId as the selected value. resolveInboundPeerIdentity later trims that final value to null, so a high-priority not-found hook can block a lower-priority resolver from supplying the real identity.
    Confidence: 0.86
  • [P2] Keep Slack system events on resolved DM sessions — extensions/slack/src/monitor/message-handler/prepare.ts:344-352
    Normal Slack DMs would route to the canonical peer session, but block-action and modal system events still resolve from the raw sender ID in the existing synchronous system-event path. Interactive DM history can split between canonical and raw Slack-user sessions.
    Confidence: 0.85
  • [P2] Thread resolved peer IDs into WhatsApp broadcasts — extensions/whatsapp/src/auto-reply/monitor/on-message.ts:78
    The primary WhatsApp route receives the resolved peer ID, but broadcast fan-out still rebuilds per-agent session keys from the raw peer ID. Direct chats with broadcast enabled can split the main reply path and broadcast agents across different sessions.
    Confidence: 0.85
  • [P3] Add the required changelog entry — docs/automation/hooks.md:370
    This introduces a user-facing plugin API hook and documented behavior, but the PR file list does not include CHANGELOG.md. Repo policy requires user-facing feature changes to carry a changelog entry before merge.
    Confidence: 0.82

Overall correctness: patch is incorrect
Overall confidence: 0.88

Security concerns:

  • [medium] Document and test plugin-controlled DM session merging — src/routing/session-key.ts:146
    A plugin-provided canonical peer ID takes precedence over static identityLinks for direct-message session keys. Because these keys isolate transcripts and routing state, the hook contract needs clear warnings, failure/blank semantics, and regression coverage for accidental user-collision behavior.
    Confidence: 0.84

What I checked:

  • Current main lacks the requested hook surface: A repository-wide symbol search on current main found no before_identity_resolve, resolveInboundPeerIdentity, runBeforeIdentityResolve, canonicalPeerId, or resolvedPeerId symbols. (ea75cd897182)
  • Hook contracts now live in hook-types: Current main defines PluginHookName, PLUGIN_HOOK_NAMES, PluginHookHandlerMap, and PluginHookRegistration in src/plugins/hook-types.ts, so adding a hook only to types.ts is stale. (src/plugins/hook-types.ts:71, ea75cd897182)
  • types.ts is a re-export surface: src/plugins/types.ts imports hook contracts from hook-types.ts and re-exports that module; it is no longer the source of truth for hook-name validation or handler maps. (src/plugins/types.ts:100, ea75cd897182)
  • Current modifying-hook runner expects a policy object: runModifyingHook takes a ModifyingHookPolicy object with mergeResults and optional shouldStop; the PR diff passes a bare merge callback at the new identity hook call site. (src/plugins/hooks.ts:190, ea75cd897182)
  • Current routing has no resolved peer input: ResolveAgentRouteInput and buildAgentSessionKey currently accept only raw peer IDs and identityLinks; the PR is adding a new route/session-key input, not fixing an already-present hook. (src/routing/resolve-route.ts:33, ea75cd897182)
  • Current docs only document static identityLinks: The session docs currently tell users to use session.identityLinks for cross-channel DM identity linking and do not document a dynamic plugin hook. Public docs: docs/concepts/session.md. (docs/concepts/session.md:53, ea75cd897182)

Likely related people:

  • Eva: Recent commits touch src/plugins/hook-types.ts and src/plugins/hooks.ts, the current contract surface this PR must rebase onto. (role: recent plugin hook contract maintainer; confidence: high; commits: cb3853587576, 8afc9ef73cb5; files: src/plugins/hook-types.ts, src/plugins/hooks.ts)
  • Vincent Koc: Recent plugin SDK/hook refactors and local history/blame around routing and affected channel files make this a likely routing candidate. (role: adjacent plugin SDK and routing maintainer; confidence: medium; commits: 546edcaa03f3, dfed74b2546d, a7c5a0425988; files: src/plugins/hooks.ts, src/routing/session-key.ts, extensions/slack/src/monitor/message-handler/prepare.ts)
  • Peter Steinberger: Recent session/plugin-SDK work plus security-sensitive MS Teams hardening make this a likely maintainer route for the public API and session-isolation decision. (role: maintainer and API/session reviewer; confidence: medium; commits: c56b56e514f8, a71b810e43; files: src/plugin-sdk, src/routing, extensions/msteams/src/monitor-handler/message-handler.ts)
  • Marcus Castro: Authored the recent WhatsApp multi-account inbound change that touched the same on-message and broadcast files implicated by the PR propagation gap. (role: WhatsApp adjacent owner; confidence: high; commits: 458a52610a4d; files: extensions/whatsapp/src/auto-reply/monitor/on-message.ts, extensions/whatsapp/src/auto-reply/monitor/broadcast.ts)
  • @zimeg: Recent Slack command/thread work appears adjacent to the Slack DM and slash-command routing touched by this PR. (role: Slack adjacent maintainer; confidence: medium; commits: 1f14c8d96b, d35bdf6311; files: extensions/slack/src/monitor/slash.ts, extensions/slack/src/monitor/message-handler/prepare.ts)

Remaining risk / open question:

  • The new hook lets plugin code choose canonical DM peer IDs, so a buggy or compromised resolver can collapse transcript isolation across distinct users unless the trust contract is explicit.
  • The branch is stale against current hook-contract and SDK-boundary changes, so a rebase may expose additional conflicts beyond the source-level blockers identified here.

Codex review notes: model gpt-5.5, reasoning high; reviewed against ea75cd897182.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

channel: msteams Channel integration: msteams channel: slack Channel integration: slack channel: whatsapp-web Channel integration: whatsapp-web docs Improvements or additions to documentation gateway Gateway runtime size: M

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant