feat(agent-collab): Phase 1 + early Phase 2 — agent-dm pod type, sharePod auth, codex preset - #275
Closed
samxu01 wants to merge 5 commits into
Closed
feat(agent-collab): Phase 1 + early Phase 2 — agent-dm pod type, sharePod auth, codex preset#275samxu01 wants to merge 5 commits into
samxu01 wants to merge 5 commits into
Conversation
…gged autoJoin
Implements Phase 1 of docs/plans/agent-collaboration-surfaces.md:
- New `agent-dm` Pod.type for any 2-member DM (any combination of
user / bot). Same-diff allow-list adds in messageController.ts and
agentMentionService.ts so messages don't silent-drop on the new
type. Bot senders are allowed in agent-dm (the whole point) but
still blocked in legacy agent-admin/agent-room.
- `dmService.sharePod(a, b)`: single source of truth for the §3.7
co-pod-member rule. Returns true iff a and b share at least one
pod. Used by both `getOrCreateAgentDmRoom` and the autoJoin gate.
- `dmService.getOrCreateAgentDmRoom(memberA, memberB)`: creates an
agent-dm pod with both members, idempotent on the unordered pair,
AgentInstallation upserts for any bot members (heartbeat off).
- `AgentInstallation.upsert(...)`: idempotent variant of `install`
via findOneAndUpdate(upsert+setOnInsert). Mention-driven autoJoin
re-fires + later admin installs converge on a single row instead
of throwing.
- `POST /api/agents/runtime/agent-dm`: agent-runtime endpoint for
opening a 2-agent DM. Accepts target = {agentName,instanceId} |
{userId} | {alias}; resolves alias against pod.contacts then
caller's contacts; runs sharePod auth (bypassed when resolution
came from pod.contacts — admin-binding carve-out); drops
"DM started" system event in originPodId via commonly-bot.
- Mention-driven autoJoin in agentMentionService behind
ENABLE_MENTION_AUTOJOIN env flag (default off): unresolved aliases
fall through to pod.contacts → sender.contacts; if resolved
outside pod.members, runs sharePod auth + AgentInstallation.upsert
+ adds to pod.members + drops system event + fires chat.mention.
- Schema additions, all with safe defaults so existing rows don't
throw on read:
- User.contacts: [ContactEntry] (alias, agentName/instanceId or
targetUserId, role, source, pinned, addedAt) — applies to both
human and bot users.
- Pod.contacts: Map<alias, {agentName, instanceId}> for per-pod
bindings.
- AgentInstallation.config.role uses the existing free-form
config map; the new `codex` role lookups in P2 read it via
config.get('role').
- V2 sidebar treats `agent-dm` as Private; agent↔agent (both bot
members) shows an "AA" icon to distinguish from human DMs.
- `getAllPods` extends membership-filter to agent-dm so personal
pod types stay user-scoped (matches existing agent-room rule).
Tests:
- DMService.sharePod (true/false/bot-bot/bot-human/self).
- getOrCreateAgentDmRoom (create, idempotent on unordered pair,
AgentInstallation rows for both bots, self-DM refused, naming).
- AgentInstallation.upsert idempotency + reactivation of
uninstalled rows.
- enqueueDmEvent allow-list for agent-dm + bot-sender allowed in
agent-dm but blocked in agent-admin.
All 25 unit/service tests pass. tsc:check clean.
Phase 2 (clawdbot codex preset + heartbeat cutover) lands next.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase 2 of docs/plans/agent-collaboration-surfaces.md:
- New `codex` agent preset on the openclaw runtime — same shape as
`engineering-copilot`, no heartbeat (reactive only), runs gpt-5.4-mini
via the LiteLLM rotator. Persona is a code-quality-focused
collaborator, not a CLI / not a runtime — it IS the agent. Replaces
the operator-specific `sam-local-codex` stop-gap that broke when
account-2/3 tokens were refreshed for the rotator.
- `PATCH /api/pods/:id/contacts` — pod-level alias bindings for the
new pod.contacts Map. Body `{ contacts: { codex: { agentName,
instanceId } | null } }`. Pod-member-only auth; setting an alias to
null deletes it. Single endpoint for all aliases (codex, claude,
reviewer, …) so we don't fragment the API.
Phase 2 still pending in this PR scope:
- Manage-tab UI to expose the PATCH endpoint (frontend follow-up).
- Heartbeat cutover for Pixel/Theo/Ops (registry.ts heartbeat
templates) — depends on the codex agent being installed in the
demo pods, which is a runtime-config step not a code change.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… users Three Important findings from the self-review pass on 2baa52d: 1. PATCH /api/pods/:id/contacts was member-gated. The contacts map is the admin-binding carve-out that lets `@codex` resolve outside sharePod for mention-driven autoJoin (§3.4). Member-only writes would let any pod member pin an arbitrary agent and have the resolver bypass the co-pod-member rule. Now requires pod creator OR global admin (db lookup; we don't trust JWT-only role claims). 2. The mention-driven autoJoin path fetched pod.contacts twice — once to compute `fromPodBinding` and once inside resolveContactAlias. An admin removing a binding between the two reads would let the stale `fromPodBinding=true` skip sharePod auth even though the resolution then fell to sender.contacts. Refactored resolveContactAlias to accept the already-fetched pod.contacts and return the source ('pod' | 'sender'); the gate uses that single snapshot for both lookup and auth. 3. POST /api/agents/runtime/agent-dm with target={agentName} called getOrCreateAgentUser unconditionally — a typo would materialize a permanent ghost bot User row. The legacy /room endpoint had the same shape but we explicitly choose stricter behavior on the new endpoint: probe via User.findOne first; 404 on miss with the resolved name in the error body so callers can act on it. Also tightened the AgentInstallation.upsert reactivation test to verify identity fields (agentName, podId, instanceId) are unchanged on the reactivation path — proves the unique-index filter matched rather than a coincidental ObjectId collision. All 25 unit + service tests still passing; tsc:check clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… invariants Updates docs/agents/AGENT_RUNTIME.md to reflect the Phase 1 changes: - DM auto-route allow-list now lists all three pod types (agent-admin, agent-room, agent-dm) and explains the bot-sender carve-out for agent-dm. - Adds the new agent-dm and mention-autoJoin paths to the membership-vs-install table. - Calls out `AgentInstallation.install` vs `upsert` semantics so the next person picking either method knows the rule of thumb. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ery coercion, format string
CodeQL flagged three real issues against the new endpoints + service code:
- POST /api/agents/runtime/agent-dm and PATCH /api/pods/:id/contacts
had no rate limiter. Added `phase4RateLimit` (existing helper) on the
agent-dm route and a fresh `podAdminRateLimit` (60/min/IP) on the
contacts route — the contacts endpoint isn't volume-sensitive but
shouldn't be unbounded either.
- `Pod.findById(originPodId)` and `Pod.findById(originPodId)` (twice in
the agent-dm endpoint) read directly from req.body. Without
coercion, a request body of `{ originPodId: { $gt: '' } }` slips
through Mongoose's implicit object-as-query interpretation. Coerced
to a string up front and use the coerced value at all three call
sites (resolution, auth check, system-message post).
- `dmService.getOrCreateAgentDmRoom` interpolated `member.agentName`
(request-derived, lowercased but still user-controlled) into a
console.error format string. Restructured to pass values as
separate console arguments so user input can't reach a format spec.
All 25 unit + service tests still pass; tsc:check clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
| * Request: { target: { agentName, instanceId? } | { userId } | { alias }, originPodId? } | ||
| * Response: { room, autoJoined: bool } | ||
| */ | ||
| router.post('/agent-dm', agentRuntimeAuth, phase4RateLimit, async (req: any, res: any) => { |
Contributor
Author
|
Merged to main as d5b7198 via local squash (per project convention — see feedback-pr-merge-pattern memory). |
This was referenced May 3, 2026
samxu01
added a commit
that referenced
this pull request
May 3, 2026
Test & Coverage was red on every PR since #275 due to stale fixtures, not stale code. Three test files updated: 1. podController.test.js — populate signatures now include `isBot`; add DM_POD_TYPES_GUARD to agentIdentityService jest mock so joinPod's runtime require resolves the ADR-001 §3.10 guard. 2. dmService.test.ts — agent-room labels now come from User.botMetadata .displayName (per f9ff990) instead of `<runtime> (<instance>)`. Update fixture and replace one stale test with two intent-aligned ones (displayName overrides args; instanceId is fallback). 3. registry.list-agents-config.test.js — add jest mocks for User, dmService, agentIdentityService so the 10s timeout (Mongoose User.find against unconnected DB) goes away. Pure test repair, zero production changes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
samxu01
added a commit
that referenced
this pull request
May 4, 2026
Fills the human-facing UX for the agent-dm pod type shipped in PR #275. Gap #1 — Read-only banner + composer disable Humans with §3.7 read-access (sharing a pod with one of the bots) can VIEW but not POST in agent-dm rooms. Without this, the composer accepts input and silently 401s on send. Now: composer is replaced with a soft-toned banner ("Read-only — you're observing this conversation"). Bot-bot variant phrases it as "X and Y are talking directly. To engage them, @-mention either in a shared team pod." Gap #2 — System event card for "🤝 X and Y started a DM" The §3.8 announcement post commonly-bot drops in a team pod when two agents start a DM rendered as raw markdown — including a `[view](/v2/pods /<id>)` link that opened in a new tab (markdown anchors default to `target="_blank"`, which is wrong for in-app navigation). Detect by content shape (no metadata column on PG messages yet), render as a chrome-light card with router-aware `navigate(...)` button. Gap #3 — "Between agents · N" sub-section in Private filter Bot↔bot rooms (where the human is read-only) were mixed in with regular DMs in the Private filter, hiding the relationship. Override `groupPods` when filter==='private': split into Pinned, Direct messages, and Between agents · N — the count doubles as a glanceable "how much agent-to-agent activity is happening" indicator. Gap #4 — Empty state copy for fresh agent-dm rooms Generic "Talk to your team" copy isn't helpful in a 1:1 DM context. Branch on bot-bot vs human-bot to render appropriate empty-state text ("X and Y haven't talked yet — they'll DM each other when one of them needs the other's help" vs "No messages yet — this is a private 1:1 conversation"). Design review per CLAUDE.md: no gradients, borders not shadows, one accent. Token discipline (use --v2-bg-subtle not undefined --v2-surface -soft). v2-root button.X specificity prefix on the syscard CTA. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Implements Phase 1 + early Phase 2 of
docs/plans/agent-collaboration-surfaces.md.Summary
Phase 1 (the demo blocker)
agent-dmPod.type — any 2-member DM (any combination of user/bot).messageController.tsandagentMentionService.ts:enqueueDmEventso the new pod type doesn't silent-drop. Bot senders are intentionally allowed inagent-dm(the whole point) but still blocked in legacyagent-admin.dmService.sharePod(a, b)— single source of truth for the §3.7 co-pod-member rule.dmService.getOrCreateAgentDmRoom(memberA, memberB)— idempotent on the unordered pair, AgentInstallation upserts for any bot members (heartbeat off).AgentInstallation.upsert(...)— new static viafindOneAndUpdate(upsert+setOnInsert). Re-fires + later admin installs converge on a single row instead of throwing or duplicating.POST /api/agents/runtime/agent-dm— accepts target ={agentName,instanceId}|{userId}|{alias}; resolves alias againstpod.contactsthen caller's contacts; runssharePodauth (bypassed only when resolution came from pod.contacts — admin-binding carve-out); drops "DM started" system event inoriginPodId.agentMentionServicebehindENABLE_MENTION_AUTOJOINenv flag (default off): unresolved aliases fall through to pod.contacts → sender.contacts; if resolved outsidepod.members, runs sharePod (same admin-binding carve-out) + AgentInstallation.upsert + adds to pod.members + drops system event + fires chat.mention.User.contacts: [ContactEntry]for both human and bot users.Pod.contacts: Map<alias, {agentName, instanceId}>for per-pod bindings.AgentInstallation.config.rolereuses existing free-form config map.agent-dmas Private; agent↔agent (both bot members) gets an "AA" icon to distinguish from human DMs.getAllPodsextends membership filter toagent-dmso personal pod types stay user-scoped.Phase 2 substrate
codexagent preset on the openclaw clawdbot gateway — code-quality-focused, reactive (no heartbeat), routes through LiteLLM via the codex-auth-rotator already live. Replaces the operator-specificsam-local-codexstop-gap that broke when account-2/3 tokens were refreshed for the rotator.PATCH /api/pods/:id/contacts— pod creator OR global admin only (intentionally tighter than member-only; see Self-review Add basic unit tests for backend functions #1 below). One endpoint for all aliases (codex/claude/reviewer/…).Out of scope for this PR:
Self-review pass
Ran the code-reviewer agent on
2baa52d266. Three Important findings, all addressed ina186406855:sharePod. Tightened to creator OR global admin (db lookup; we don't trust JWT-only role claims).'pod' | 'sender') throughresolveContactAlias.target.agentName— typos would create permanent bot User rows. Probe viaUser.findOnefirst; 404 on miss.Reviewer also flagged a Question about backfilling existing
agent-roomrows missing AgentInstallation — that's a runtime fix using the existing kubectl-exec script frome78b5df241, not blocked by this PR.Test plan
dmService.agentDm.test.ts+ extendedagentMentionService.test.js).tsc:checkclean./v2/pods(no socket-auth flood), POST to/api/agents/runtime/agent-dmwith valid + invalid agentName (404 on miss, no ghost row), confirm allow-list lets messages route to the agent in anagent-dmpod.codexpreset in a demo pod and pin viaPATCH /pods/:id/contactsto verify the resolution chain end-to-end.🤖 Generated with Claude Code