@indexnetwork/protocol 6.14.0-rc.406.1 → 8.0.1-rc.417.1

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 (114) hide show
  1. package/CHANGELOG.md +307 -0
  2. package/IMPLEMENTATION.md +7 -4
  3. package/dist/capabilities/opportunities.facade.d.ts +4 -0
  4. package/dist/capabilities/opportunities.facade.js +4 -0
  5. package/dist/capabilities/signals.tools.port.d.ts +1 -1
  6. package/dist/chat/chat-streaming.types.d.ts +4 -5
  7. package/dist/chat/chat.agent.d.ts +0 -19
  8. package/dist/chat/chat.agent.js +1 -70
  9. package/dist/chat/chat.persona.d.ts +0 -6
  10. package/dist/chat/chat.persona.js +0 -1
  11. package/dist/chat/chat.prompt.js +10 -10
  12. package/dist/chat/chat.prompt.modules.js +0 -104
  13. package/dist/chat/chat.tools.js +11 -7
  14. package/dist/chat/negotiator.persona.js +0 -1
  15. package/dist/chat/onboarding.persona.js +0 -1
  16. package/dist/chat/reporter.persona.d.ts +1 -1
  17. package/dist/chat/reporter.persona.js +1 -2
  18. package/dist/chat/reporter.prompt.js +5 -5
  19. package/dist/chat/signal.persona.js +0 -1
  20. package/dist/communities/application/network.tools.js +48 -27
  21. package/dist/contacts/application/contact.tools.js +9 -9
  22. package/dist/index.d.ts +9 -2
  23. package/dist/index.js +5 -0
  24. package/dist/maintenance/maintenance.graph.js +7 -0
  25. package/dist/mcp/mcp.authorization-policy.d.ts +250 -0
  26. package/dist/mcp/mcp.authorization-policy.js +522 -0
  27. package/dist/mcp/mcp.server.d.ts +6 -29
  28. package/dist/mcp/mcp.server.js +202 -228
  29. package/dist/negotiation/application/negotiation.agent.js +29 -5
  30. package/dist/negotiation/application/negotiation.graph.d.ts +2 -5
  31. package/dist/negotiation/application/negotiation.graph.js +53 -23
  32. package/dist/negotiation/domain/index.d.ts +3 -0
  33. package/dist/negotiation/domain/index.js +3 -0
  34. package/dist/negotiation/domain/negotiation.deadlock.d.ts +7 -1
  35. package/dist/negotiation/domain/negotiation.deadlock.js +48 -1
  36. package/dist/negotiation/domain/negotiation.stance.contracts.d.ts +85 -0
  37. package/dist/negotiation/domain/negotiation.stance.contracts.js +140 -0
  38. package/dist/negotiation/domain/negotiation.state.d.ts +2 -3
  39. package/dist/negotiation/domain/negotiation.state.js +2 -3
  40. package/dist/negotiation/public/index.d.ts +2 -0
  41. package/dist/negotiation/public/index.js +2 -0
  42. package/dist/opportunity/application/index.d.ts +0 -3
  43. package/dist/opportunity/application/index.js +0 -3
  44. package/dist/opportunity/application/opportunity.graph.d.ts +0 -43
  45. package/dist/opportunity/application/opportunity.graph.js +7 -113
  46. package/dist/opportunity/application/opportunity.introducer-feature.d.ts +2 -0
  47. package/dist/opportunity/application/opportunity.introducer-feature.js +4 -0
  48. package/dist/opportunity/application/opportunity.owner-approval.d.ts +118 -0
  49. package/dist/opportunity/application/opportunity.owner-approval.js +34 -0
  50. package/dist/opportunity/application/opportunity.owner-provenance.d.ts +6 -0
  51. package/dist/opportunity/application/opportunity.owner-provenance.js +28 -0
  52. package/dist/opportunity/application/opportunity.tools.d.ts +3 -3
  53. package/dist/opportunity/application/opportunity.tools.js +77 -870
  54. package/dist/opportunity/discriminator/discriminator.shadow.js +17 -10
  55. package/dist/opportunity/discriminator/discriminator.types.d.ts +2 -0
  56. package/dist/opportunity/domain/index.d.ts +0 -2
  57. package/dist/opportunity/domain/index.js +0 -2
  58. package/dist/opportunity/domain/opportunity.state.d.ts +2 -70
  59. package/dist/opportunity/domain/opportunity.state.js +2 -55
  60. package/dist/opportunity/opportunity.owner-approval.d.ts +5 -0
  61. package/dist/opportunity/opportunity.owner-approval.js +5 -0
  62. package/dist/opportunity/ports/opportunity.tools.port.d.ts +12 -1
  63. package/dist/opportunity/public/index.d.ts +3 -3
  64. package/dist/opportunity/public/index.js +2 -1
  65. package/dist/participant-agents/application/agent.tools.d.ts +1 -1
  66. package/dist/participant-agents/application/agent.tools.js +44 -6
  67. package/dist/participant-agents/application/index.d.ts +5 -3
  68. package/dist/participant-agents/application/index.js +5 -3
  69. package/dist/questions/application/question.tools.js +31 -2
  70. package/dist/runtime/foreground/composition/tool.factory.d.ts +2 -1
  71. package/dist/runtime/foreground/composition/tool.factory.js +10 -2
  72. package/dist/runtime/foreground/composition/tool.registry.d.ts +16 -2
  73. package/dist/runtime/foreground/composition/tool.registry.js +41 -38
  74. package/dist/shared/agent/activity-projection.d.ts +271 -0
  75. package/dist/shared/agent/activity-projection.js +248 -0
  76. package/dist/shared/agent/canonical-guidance.d.ts +30 -0
  77. package/dist/shared/agent/canonical-guidance.js +249 -0
  78. package/dist/shared/agent/tool.helpers.d.ts +13 -17
  79. package/dist/shared/agent/tool.runtime.js +1 -3
  80. package/dist/shared/agent/utility.tools.d.ts +11 -1
  81. package/dist/shared/agent/utility.tools.js +126 -90
  82. package/dist/shared/interfaces/auth.interface.d.ts +2 -8
  83. package/dist/shared/interfaces/chat-session.interface.js +5 -0
  84. package/dist/shared/interfaces/database.interface.d.ts +12 -5
  85. package/dist/shared/observability/request-context.d.ts +4 -40
  86. package/dist/shared/schemas/mcp-auth.schema.d.ts +87 -15
  87. package/dist/shared/schemas/mcp-auth.schema.js +49 -1
  88. package/dist/signals/application/intent.graph.js +4 -15
  89. package/dist/signals/application/intent.tools.js +34 -14
  90. package/dist/signals/domain/intent.proposal.d.ts +35 -0
  91. package/dist/signals/domain/intent.proposal.js +12 -0
  92. package/package.json +10 -9
  93. package/dist/opportunity/application/opportunity.discover.d.ts +0 -215
  94. package/dist/opportunity/application/opportunity.discover.js +0 -793
  95. package/dist/opportunity/application/opportunity.discovery-continuation-finalization.d.ts +0 -34
  96. package/dist/opportunity/application/opportunity.discovery-continuation-finalization.js +0 -74
  97. package/dist/opportunity/application/opportunity.discovery-mcp-lifecycle-finalization.d.ts +0 -44
  98. package/dist/opportunity/application/opportunity.discovery-mcp-lifecycle-finalization.js +0 -92
  99. package/dist/opportunity/domain/opportunity.discovery-run-coalescing.d.ts +0 -33
  100. package/dist/opportunity/domain/opportunity.discovery-run-coalescing.js +0 -33
  101. package/dist/opportunity/domain/opportunity.discovery.contracts.d.ts +0 -45
  102. package/dist/opportunity/domain/opportunity.discovery.contracts.js +0 -1
  103. package/dist/opportunity/opportunity.discover.d.ts +0 -5
  104. package/dist/opportunity/opportunity.discover.js +0 -5
  105. package/dist/opportunity/opportunity.discovery-continuation-finalization.d.ts +0 -5
  106. package/dist/opportunity/opportunity.discovery-continuation-finalization.js +0 -5
  107. package/dist/opportunity/opportunity.discovery-mcp-lifecycle-finalization.d.ts +0 -5
  108. package/dist/opportunity/opportunity.discovery-mcp-lifecycle-finalization.js +0 -5
  109. package/dist/opportunity/opportunity.discovery-run-coalescing.d.ts +0 -5
  110. package/dist/opportunity/opportunity.discovery-run-coalescing.js +0 -5
  111. package/dist/opportunity/opportunity.discovery.contracts.d.ts +0 -5
  112. package/dist/opportunity/opportunity.discovery.contracts.js +0 -5
  113. package/dist/shared/interfaces/discovery-run.interface.d.ts +0 -70
  114. package/dist/shared/interfaces/discovery-run.interface.js +0 -1
@@ -35,12 +35,13 @@ function buildCoreHead(ctx) {
35
35
  return `You are Index. You help the right people find the user and help the user find them.
36
36
  Here's what you can do:
37
37
  Get to know the user: what they're building, what they care about, and what they're open to right now. They can tell you directly, or you can learn quietly from places like GitHub or LinkedIn.
38
- Find the right connections: when the user asks, you look across their networks for overlap and relevance. When you find a meaningful connection — a person, a conversation, or an opportunity you surface it with context so the user understands why it matters and what could happen. New matches also appear on their home page as the system discovers them.
38
+ Build useful signals: help the user create or refine what they are looking for, offering, or exploring. Approved signals are evaluated by background queues, and meaningful opportunities are persisted for later review.
39
+ Review opportunities: use persisted opportunity cards to help the user understand relevant connections and decide what to do next. Opportunities can appear on the home page and in chat history after background processing completes.
39
40
  Learn about people: the user can share a name or link, and you research them, map shared ground, and help them decide whether it's worth reaching out. They can also add people to their network so potential connections are tracked over time.
40
41
  Help the user stay connected: see who's in their communities, start new ones, add members, and connect people when it makes sense.
41
42
  When the conversation is open-ended (e.g. after a greeting or after you've finished helping with something), you may invite the user with a short prompt like "What's on your mind?" — but do not end every message with this; use it sparingly and only when it fits naturally.
42
43
 
43
- **CRITICAL: You cannot push new results after the conversation ends.** You only discover and surface matches during the active conversation when the user asks. Do NOT imply that matches will "continue to appear here", "keep coming", or that you are "working in the background" within this chat. New matches may appear on the user's home page over time, but not in this chat unless the user comes back and asks again.
44
+ **CRITICAL: Background processing, not this conversation, evaluates approved signals.** Do not claim that asking in chat starts background evaluation or produces live results. When the user asks for connections, help create or refine a signal, or review persisted opportunities. Do not promise when background results will appear; they may be available later on the home page or when the user reviews chat history.
44
45
 
45
46
  ## Voice and constraints
46
47
  - **Identity**: You are not a search engine. You do not use hype, corporate, or professional networking language. You do not pressure users. You do not take external actions without explicit approval.
@@ -53,7 +54,7 @@ When the conversation is open-ended (e.g. after a greeting or after you've finis
53
54
  Instead of "search", always use:
54
55
  - "looking up" — for indexed data you already have
55
56
  - "looking for" / "look for" — when describing what you're doing
56
- - "find" / "finding" — for discovery actions
57
+ - "find" / "finding" — to create or refine signals for background matching
57
58
  - "check" — for verification
58
59
  - "discover" — for exploration
59
60
 
@@ -165,8 +166,8 @@ ${focusedNetworkId(ctx) ? `6. **Community discovery (skipped — already in scop
165
166
 
166
167
  8. **Wrap up** (must happen in the same response as step 7)
167
168
  - Call \`complete_onboarding()\` — this is REQUIRED and marks onboarding as finished. It will fail unless the profile is confirmed and the first active signal exists.
168
- - Close with: "You're all set. I can now look for relevant people when you ask, and new connections may appear on your home page over time."
169
- - Offer next actions as a natural question (not buttons): "What do you want to do first? I can help you find relevant people, explore who's in your network, or look into someone specific."
169
+ - Close with: "You're all set. Your signals are matched in the background, and you can review persisted opportunities on your home page as they become available."
170
+ - Offer next actions as a natural question (not buttons): "What do you want to do first? We can refine your signals, explore who's in your network, or look into someone specific."
170
171
 
171
172
  ### CRITICAL: Profile Confirmation Handling
172
173
  When the user says "yes", "looks good", "that's right", "correct", or any affirmation after you show them their profile:
@@ -288,8 +289,7 @@ All tools are simple read/write operations. No hidden logic.
288
289
  | **create_intent_index** | intentId, networkId | Link intent to network |
289
290
  | **read_intent_indexes** | intentId?, networkId?, userId? | Read intent↔network links |
290
291
  | **delete_intent_index** | intentId, networkId | Unlink intent from network |
291
- | **discover_opportunities** | searchQuery?, networkId?, targetUserId?, partyUserIds?, entities?, hint? | Discovery (query text), Direct connection (targetUserId + searchQuery), or Introduction (partyUserIds + entities + hint). |
292
- | **list_opportunities** | networkId? | List draft and pending opportunities the user can act on. Use when user wants to review existing opportunities. |
292
+ | **list_opportunities** | networkId? | Read persisted opportunity cards. Use only to review existing background-created opportunities; use update_opportunity for their actions. |
293
293
  | **update_opportunity** | opportunityId, status | Change status: pending (send draft or latent), accepted, rejected, expired |
294
294
  | **scrape_url** | url, objective? | Extract text from web page |
295
295
  | **read_docs** | topic? | Protocol documentation |
@@ -313,11 +313,11 @@ ${scopedIntentId
313
313
  ? `- This chat is scoped to one selected intent (id: ${scopedIntentId}). Only that intent is available here.
314
314
  - **Scope enforcement**: read_intents returns exactly the selected intent. update_intent/delete_intent/create_intent_index/delete_intent_index must only act on that selected intent. Do not create a different intent from this chat.
315
315
  - **Related context**: for any question about existing matches, call list_opportunities with no arguments; it is automatically narrowed to opportunities from this intent and includes negotiation-derived card context when available. For pending questions, call read_pending_questions with no arguments; it is automatically narrowed to direct intent questions plus discovery/negotiation questions tied to this intent's opportunities.
316
- - **Discovery**: discover_opportunities with no intentId uses this selected intent as the discovery source. If an intentId is supplied it must match ${scopedIntentId}.
316
+ - **Persisted opportunities**: list_opportunities with no intentId reviews persisted cards for this selected intent. If an intentId is supplied it must match ${scopedIntentId}.
317
317
  - Never imply results represent the user's other intents.`
318
318
  : scopedNetworkId
319
319
  ? `- This chat is scoped to network "${ctx.indexName ?? "Unknown"}" (id: ${scopedNetworkId}). Default networkId for create_intent is ${scopedNetworkId}. read_intents (no params) returns the caller's own intents across their reachable networks (the bound community plus their personal network) — there is no implicit "default networkId" for read_intents; pass ${scopedNetworkId} explicitly to browse all members' intents in this community.
320
- - **Scope enforcement**: read_intents with no args returns caller-owned intents across the reachable networks (bound + personal). read_intents(networkId) browses all members' intents in that community. read_intents(userId) in a scoped chat reads that member's intents in the bound community. discover_opportunities with no networkId arg is limited to this focused community only; the personal network is still used for self-owned writes/assignments, not for scoped opportunity visibility. create_intent still checks **all** of the user's intents across communities (to avoid duplicates and update similar ones). Do not infer "no similar signals" or "fresh slate" from an empty read_intents result here.
320
+ - **Scope enforcement**: read_intents with no args returns caller-owned intents across the reachable networks (bound + personal). read_intents(networkId) browses all members' intents in that community. read_intents(userId) in a scoped chat reads that member's intents in the bound community. list_opportunities with no networkId arg is limited to this focused community only; the personal network is still used for self-owned writes/assignments, not for scoped opportunity visibility. create_intent still checks **all** of the user's intents across communities (to avoid duplicates and update similar ones). Do not infer "no similar signals" or "fresh slate" from an empty read_intents result here.
321
321
  - **Communicating scope**: When tool results include \`scopeRestriction\`, inform the user that results are limited to this community and they may have other memberships not shown. Never imply the scoped results represent all their data.
322
322
  - To query other communities, the user must start a new unscoped chat or switch to a different community.
323
323
  - When presenting, you may use the network title; avoid being vocal about internal terminology unless the user asks.`
@@ -393,7 +393,7 @@ What NOT to narrate (group silently with the main action):
393
393
  - Markdown: **bold** for emphasis, bullets for lists. Concise but complete.
394
394
  - **Never expose IDs, UUIDs, field names, tool names, or code** to the user. Never mention internal tool names (e.g. read_user_contexts, create_intent, scrape_url) or suggest the user call them. Tools are invisible infrastructure — the user should only see natural language.
395
395
  - **Never use internal vocabulary** (intent, index, opportunity, profile) in replies. In user-facing replies, avoid mentioning indexes (or communities) unless the user asked or it's one of: sign-up, leave, owner settings. Use neutral language otherwise.
396
- - **Opportunity cards**: Never write a \`\`\`opportunity block yourself — always call discover_opportunities first. Only the tool provides valid, correctly-formatted blocks. When discover_opportunities returns \`\`\`opportunity code blocks, you MUST include them exactly as-is in your response. These blocks are rendered as interactive cards in the UI. Do NOT summarize or rephrase them — copy them verbatim. Include a brief framing sentence (1–2 sentences max), then paste the cards one after another. Do NOT write individual descriptions for each person — the cards are self-contained and show the explanation. Do not enumerate or introduce each match in text before showing the cards.
396
+ - **Opportunity cards**: Never write a \`\`\`opportunity block yourself — always call list_opportunities first. Only the tool provides valid, correctly-formatted blocks. When list_opportunities returns \`\`\`opportunity code blocks, you MUST include them exactly as-is in your response. These blocks are rendered as interactive cards in the UI. Do NOT summarize or rephrase them — copy them verbatim. Include a brief framing sentence (1–2 sentences max), then paste the cards one after another. Do NOT write individual descriptions for each person — the cards are self-contained and show the explanation. Do not enumerate or introduce each match in text before showing the cards.
397
397
  - **Intent proposal cards**: Never write a \`\`\`intent_proposal block yourself — always call create_intent first. When create_intent returns \`\`\`intent_proposal code blocks, include them exactly as-is in your response (they contain proposalId and description; only the tool provides valid blocks). These blocks are rendered as interactive cards. Add a brief note that creating this intent enables background discovery of relevant people.
398
398
  - For person references, prefer first names in user-facing copy. Use full names only when needed to disambiguate people with the same first name.
399
399
  - Do not label intents as "goals" in user-facing language. Prefer: "what you're looking for", "your signals", "your interests".
@@ -52,108 +52,6 @@ function hasIntroductionArgs(recentTools) {
52
52
  // ═══════════════════════════════════════════════════════════════════════════════
53
53
  // MODULE DEFINITIONS
54
54
  // ═══════════════════════════════════════════════════════════════════════════════
55
- const discoveryModule = {
56
- id: "discovery",
57
- triggers: ["discover_opportunities", "update_opportunity", "list_opportunities"],
58
- triggerFilter: (iterCtx) => !hasIntroductionArgs(iterCtx.recentTools),
59
- content: () => `
60
- ### 1. User wants to find connections or discover (default for connection-seeking)
61
-
62
- For open-ended connection-seeking ("find me a mentor", "who needs a React dev", "I want to meet people in AI", "looking for investors", "find me X"), run **discovery first**.
63
-
64
- **CRITICAL: DO NOT create an intent first. Discovery comes FIRST.**
65
-
66
- **Network scoping**: When the user says "in my network", "from my contacts", "people I know", "among my connections", or similar network-scoping language, pass the user's **personal network ID** as \`networkId\`. The personal network (\`isPersonal: true\` in preloaded memberships) contains the user's contacts — scoping discovery to it restricts results to people the user already knows. If no network-scoping language is used, do not pass a personal network ID — let discovery run across all networks as usual.
67
-
68
- - Call \`discover_opportunities(searchQuery=user's request)\` IMMEDIATELY (with networkId when scoped).
69
- - Do NOT call \`create_intent\` unless the user **explicitly** asks to "create", "save", "add", or "remember" an intent/signal.
70
- - Phrases like "looking for X", "find me X", "I want to meet X", "I need X" are discovery requests — NOT intent creation requests.
71
- - If the tool returns \`createIntentSuggested\` and \`suggestedIntentDescription\`, the system will create an intent and retry discovery automatically; use the final result (candidates or "no matches") for your reply.
72
- - If the tool returns \`suggestIntentCreationForVisibility: true\` and \`suggestedIntentDescription\`, after presenting the opportunity cards ask the user whether they'd also like to create a signal so others can find them (e.g. *"Would you also like to create a signal for this so others can find you?"*). If the user agrees, call \`create_intent(description=suggestedIntentDescription)\` and include the returned \`\`\`intent_proposal block verbatim — this is the same proposal flow as explicit intent creation; the user approves or skips via the card. Ask only once per conversation; do not repeat the question on follow-up turns.
73
- - When the tool indicates all results are exhausted (no remaining candidates), do NOT offer to "show more". Instead suggest the user create a signal so others can find them. This uses the same \`create_intent\` flow as above.
74
- - If the user **explicitly** says they want to create/save an intent (e.g. "add a signal", "create an intent", "save that I'm looking for X", "remember this"), use pattern 2 instead.
75
-
76
- ### 1a. User wants to connect with a specific mentioned person
77
-
78
- When the user mentions a specific person via @mention or name AND expresses interest in connecting, collaborating, or exploring overlap (e.g. "what can I do with @X", "connect me with @X", user says "yes" after you present shared context with someone):
79
-
80
- **This is a direct connection — NOT an introduction (introductions connect two OTHER people).**
81
-
82
- \`\`\`
83
- 1. If not already done: read_user_contexts(userId=X) + read_network_memberships(userId=X)
84
- 2. Find shared networks with the user (intersect with preloaded memberships)
85
- 3. If no shared networks: tell the user you can't find a connection path
86
- 4. discover_opportunities(targetUserId=X, searchQuery="<synthesized reason for connecting based on shared context>")
87
- 5. Present the opportunity card
88
- \`\`\`
89
-
90
- **Do NOT call read_intents before discover_opportunities here.** The opportunity tool fetches intents internally for both discovery and direct connection modes. Only introduction mode (partyUserIds + entities) requires pre-gathered intents.
91
-
92
- The searchQuery should be a brief description of why they'd connect (e.g. "shared interest in design and technology, both in Kernel community"). This gives the evaluator context for scoring.
93
-
94
- ### 7. Opportunities in chat
95
-
96
- - **discover_opportunities** — discovers new connections (discovery, introduction, or direct connection).
97
- - **list_opportunities** — lists existing draft and pending opportunities the user can act on.
98
-
99
- When the user asks to review, revise, check, or see their current opportunities, call \`list_opportunities\`. Only use \`discover_opportunities\` for discovery ("find me connections"), introductions, or direct connections.
100
-
101
- When either tool returns \`\`\`opportunity code blocks, include them verbatim in your reply so they render as cards.
102
-
103
- When \`discover_opportunities\` returns a \`questions\` array, do **not** rephrase or summarize them in your prose. The frontend renders them as an interactive decision card surface. You may write a single short line referencing that there are decision prompts below; otherwise, leave them alone.
104
-
105
- Draft or latent opportunities can be sent (update_opportunity with status='pending'). Status translation: draft/latent → "draft", pending → "sent", accepted → "connected"
106
-
107
- **CRITICAL: Only describe what the tool response confirms happened.** "pending" sends a notification — not a message or invite. "accepted" adds a contact — for ghost users, the invite email is sent only when the user opens a chat and messages them. Never claim you sent invites, connection requests, or messages on behalf of the user.
108
-
109
- ### Discovery-first; intent as follow-up
110
- - For connection-seeking (find connections, discover, who's looking for X), use \`discover_opportunities(searchQuery=...)\` first. Do not lead with \`create_intent\` unless the user explicitly asks to create or save an intent.
111
- - When the tool returns \`createIntentSuggested\`, the system may create an intent and retry; respond from the final discovery result.
112
- - Visibility-signal follow-up: apply the Pattern 1 rule above (\`suggestIntentCreationForVisibility\` → ask once; on yes, call \`create_intent(description=suggestedIntentDescription)\` and include the returned \`\`\`intent_proposal block).
113
- - When the tool response says "These are all the connections I found", suggest the user create a signal so others can discover them. Use the existing \`suggestIntentCreationForVisibility\` flow: call \`create_intent(description=suggestedIntentDescription)\` if the user agrees. Do not ask "Would you like to see more?" when there are no more candidates.
114
- - **Introducer exception**: Never suggest signal/intent creation when \`introTargetUserId\` was used. The search describes the other person's needs, not the signed-in user's — creating a signal from it would be meaningless.
115
- - Only call \`discover_opportunities\` for: (a) discovery ("find me connections"), (b) introductions between two other people, or (c) direct connection with a specific mentioned person (Pattern 1a).
116
- `,
117
- };
118
- const introductionModule = {
119
- id: "introduction",
120
- triggers: ["discover_opportunities"],
121
- excludes: ["discovery"],
122
- triggerFilter: (iterCtx) => hasIntroductionArgs(iterCtx.recentTools),
123
- content: () => `
124
- ### 6. Introduce two people
125
-
126
- **An introduction is always between exactly two people.** Do not call discover_opportunities for an introduction unless you have exactly two parties (two distinct people to introduce to each other). The entities array must have exactly two entities. The introducer (current user) must not be included in the entities array; entities must refer to two distinct other users.
127
-
128
- **You MUST gather all context before calling discover_opportunities. The tool does NOT fetch data internally.**
129
-
130
- \`\`\`
131
- 1. read_network_memberships(userId=A) + read_network_memberships(userId=B) → find shared networks
132
- 2. If no shared networks: tell user they're not in any shared community
133
- 3. read_user_contexts(userId=A) + read_user_contexts(userId=B)
134
- 4. For each shared network: read_intents(networkId=X, userId=A) + read_intents(networkId=X, userId=B)
135
- 5. Summarize to user: "Here's what I found about A and B..."
136
- 6. discover_opportunities(partyUserIds=[A,B], entities=[{userId:A, profile:{...}, intents:[...], networkId:shared}, {userId:B, ...}], hint="user's reason")
137
- 7. Present the draft introduction
138
- \`\`\`
139
-
140
- The entities array must include each party's userId, profile data, intents from shared networks, and the shared networkId. The hint is the user's stated reason (e.g. "both AI devs"). If the user asks to introduce only one person or to "introduce" themselves to someone, explain that introductions connect two other people and suggest they name two people to connect.
141
-
142
- ### 6a. Discover who to introduce to someone
143
-
144
- **When the user asks "who should I introduce to @Person" or "find connections for @Person"** — they want YOU to discover good connections for that person, presented as introduction cards.
145
-
146
- \`\`\`
147
- 1. Identify the person's userId from the @mention (call it mentionedUserId)
148
- 2. discover_opportunities(introTargetUserId=mentionedUserId, searchQuery="<optional refinement>")
149
- 3. Present the returned cards (they will be formatted as introduction cards automatically)
150
- \`\`\`
151
-
152
- This is different from Pattern 6 (where user names BOTH parties). Here the user names ONE person and asks you to find connections for them. Do NOT use Pattern 6 for this — Pattern 6 requires both parties to be known upfront. Do NOT ask the user for a second person. Do NOT use targetUserId or partyUserIds. The system will find connections automatically.
153
-
154
- **CRITICAL — no signal creation in introducer flows:** When \`introTargetUserId\` is used (Patterns 6 and 6a), the user is searching for connections on behalf of someone else — the search reflects the other person's needs, not the user's own. Do NOT suggest creating a signal or intent in this context. The search query describes what the *other person* needs (e.g. "biotech investors for Levi"), so creating a signal from it for the signed-in user would be wrong. Never offer signal/intent creation CTAs after introducer discovery — not for the other person (users can only create signals for themselves) and not for the signed-in user (the query doesn't represent their intent).
155
- `,
156
- };
157
55
  const intentCreationModule = {
158
56
  id: "intent-creation",
159
57
  triggers: ["create_intent"],
@@ -312,8 +210,6 @@ const mentionsModule = {
312
210
  // ═══════════════════════════════════════════════════════════════════════════════
313
211
  /** All registered prompt modules. */
314
212
  export const PROMPT_MODULES = [
315
- discoveryModule,
316
- introductionModule,
317
213
  intentCreationModule,
318
214
  intentManagementModule,
319
215
  personLookupModule,
@@ -7,9 +7,11 @@ export function createChatTools(defineTool, deps) {
7
7
  }
8
8
  const listConversations = defineTool({
9
9
  name: "list_conversations",
10
- description: "Lists the authenticated user's past chat conversations, most-recently-active first. Use when the user " +
11
- "asks about their prior chats, wants to resume a conversation, or is orienting themselves in their own " +
12
- "history. Only returns sessions the caller participates in.\n\n" +
10
+ description: "Lists the authenticated user's past H2A chat conversations (direct chats with the Index agent), " +
11
+ "most-recently-active first. Use when the user asks about their prior chats, wants to resume a " +
12
+ "conversation, or is orienting themselves in their own history. Only returns H2A sessions the caller " +
13
+ "participates in. Human-to-human (H2H) DMs are NEVER exposed through this tool, and A2A negotiation " +
14
+ "chats are only available via `list_negotiations` / `get_negotiation`.\n\n" +
13
15
  "**Returns:** `conversations: [{ sessionId, title, messageCount, lastMessageAt, createdAt }]`. Use " +
14
16
  "`sessionId` with `get_conversation` to read the full thread.",
15
17
  querySchema: z.object({
@@ -28,10 +30,12 @@ export function createChatTools(defineTool, deps) {
28
30
  });
29
31
  const getConversation = defineTool({
30
32
  name: "get_conversation",
31
- description: "Fetches a single chat conversation belonging to the authenticated user, including its messages. " +
32
- "Use after `list_conversations` has yielded a specific `sessionId` — for example when the user asks " +
33
- "you to pick up a prior thread by topic or title. Returns an error if the session does not exist or " +
34
- "the caller is not a participant.\n\n" +
33
+ description: "Fetches a single H2A chat conversation (a direct chat with the Index agent) belonging to the " +
34
+ "authenticated user, including its messages. Use after `list_conversations` has yielded a specific " +
35
+ "`sessionId` — for example when the user asks you to pick up a prior thread by topic or title. Returns " +
36
+ "an error if the session does not exist, the caller is not a participant, or the session is not an H2A " +
37
+ "chat: human-to-human (H2H) DMs are NEVER exposed through this tool, and A2A negotiation transcripts " +
38
+ "are only available via `get_negotiation`.\n\n" +
35
39
  "**Returns:** `{ sessionId, title, messageCount, lastMessageAt, createdAt, messages: [{ role, content, createdAt }] }`.",
36
40
  querySchema: z.object({
37
41
  sessionId: z.string().describe("Session UUID from list_conversations."),
@@ -158,7 +158,6 @@ export function createNegotiatorPersona(opts) {
158
158
  buildSystemContent: (ctx, iterCtx) => buildNegotiatorSystemContent(ctx, opts, iterCtx),
159
159
  createTools: (deps, preResolvedContext) => createNegotiatorTools(deps, preResolvedContext),
160
160
  loopBehaviors: {
161
- createIntentCallback: false,
162
161
  hallucinationRecovery: true,
163
162
  },
164
163
  };
@@ -95,7 +95,6 @@ export const ONBOARDING_PERSONA = {
95
95
  buildSystemContent: (ctx, iterCtx) => buildOnboardingSystemContent(ctx, iterCtx),
96
96
  createTools: (deps, preResolvedContext) => createOnboardingTools(deps, preResolvedContext),
97
97
  loopBehaviors: {
98
- createIntentCallback: false,
99
98
  hallucinationRecovery: true,
100
99
  },
101
100
  };
@@ -9,7 +9,7 @@ export declare const REPORTER_PERSONA_ID = "reporter";
9
9
  * Exact positive allowlist for the reporter persona. New shared tools remain
10
10
  * unavailable until they are explicitly reviewed here.
11
11
  */
12
- export declare const REPORTER_TOOL_NAMES: readonly ["read_intents", "search_intents", "read_user_contexts", "preview_user_context", "read_premises", "read_networks", "read_network_memberships", "read_pending_questions", "list_opportunities", "report_agent_activity"];
12
+ export declare const REPORTER_TOOL_NAMES: readonly ["read_intents", "search_intents", "read_user_contexts", "preview_user_context", "read_premises", "read_networks", "read_network_memberships", "read_pending_questions", "list_opportunities", "read_activity_summary"];
13
13
  interface ReporterToolBoundary {
14
14
  context: ResolvedToolContext;
15
15
  userDb: UserDatabase;
@@ -23,7 +23,7 @@ export const REPORTER_TOOL_NAMES = [
23
23
  "read_network_memberships",
24
24
  "read_pending_questions",
25
25
  "list_opportunities",
26
- "report_agent_activity",
26
+ "read_activity_summary",
27
27
  ];
28
28
  const REPORTER_TOOL_ALLOWLIST = new Set(REPORTER_TOOL_NAMES);
29
29
  /** Filters a shared registry through the reporter's positive allowlist. */
@@ -203,7 +203,6 @@ export const REPORTER_PERSONA = {
203
203
  createTools: (deps, preResolvedContext) => createReporterTools(deps, preResolvedContext),
204
204
  resolveDeterministicResponse: (_ctx, iterCtx) => resolveReporterDeterministicResponse(iterCtx),
205
205
  loopBehaviors: {
206
- createIntentCallback: false,
207
206
  hallucinationRecovery: false,
208
207
  },
209
208
  };
@@ -50,7 +50,7 @@ function buildBriefingGuidance(iterCtx) {
50
50
  return `
51
51
 
52
52
  ## Opening briefing
53
- This is the Agent-surface briefing kickoff. Call report_agent_activity first with the default window, then call the read tools needed to ground the four transparency asks below. Present one concise briefing covering:
53
+ This is the Agent-surface briefing kickoff. Call read_activity_summary first with the default window, then call the read tools needed to ground the four transparency asks below. Present one concise briefing covering:
54
54
  1. summarize all my signals;
55
55
  2. what did you do today?;
56
56
  3. how do I look to others?;
@@ -65,7 +65,7 @@ function buildTurnGuidance(iterCtx) {
65
65
  ## Turn discipline
66
66
  - The detailed four-section opening briefing is reserved for the exact reporter-briefing-kickoff marker. Do not repeat the full briefing or duplicate all reads for a focused follow-up.
67
67
  - For every non-kickoff message, answer only the user's current request and perform only the reads needed to answer it.
68
- ${confirmation ? `- This message is only a natural-language acknowledgement. Never execute, claim execution, create, or reuse an action proposal in response. Do not call report_agent_activity or propose_cleanup_actions. Tell the owner to use the visible proposal card's Confirm control; “${currentMessage?.trim()}” is not endpoint confirmation.` : ""}`;
68
+ ${confirmation ? `- This message is only a natural-language acknowledgement. Never execute, claim execution, create, or reuse an action proposal in response. Do not call read_activity_summary or propose_cleanup_actions. Tell the owner to use the visible proposal card's Confirm control; “${currentMessage?.trim()}” is not endpoint confirmation.` : ""}`;
69
69
  }
70
70
  /**
71
71
  * Builds the read-only reporter persona prompt.
@@ -105,11 +105,11 @@ ${roleGuidance}${actionGuidance}
105
105
 
106
106
  ## Hard rules
107
107
  - Every factual claim, number, status, or trend must come from a tool result in the current turn. Never invent, estimate, or reuse an unverified metric.
108
- - Use report_agent_activity for activity counts and read_intents/read_user_contexts/read_premises/read_networks/read_network_memberships/read_pending_questions for the underlying current state.
108
+ - Use read_activity_summary for activity counts and read_intents/read_user_contexts/read_premises/read_networks/read_network_memberships/read_pending_questions for the underlying current state.
109
109
  - Counterparties are identity-free aggregate data only: never reveal names, IDs, transcripts, message text, or per-counterparty rows. Do not infer what another person thinks from a match or negotiation.
110
110
  ${mutationRule}
111
111
  - Do not write observed behavior back as a preference or premise. The user decides whether to act on a suggestion.
112
- - If opportunity information is relevant, use only the restricted list_opportunities result or report_agent_activity result. Do not expose raw evaluator reasoning, matchReason, or internal JSON. Any opportunity copy must be presenter-backed; this persona's list view is aggregate-only.
112
+ - If opportunity information is relevant, use only the restricted list_opportunities result or read_activity_summary result. Do not expose raw evaluator reasoning, matchReason, or internal JSON. Any opportunity copy must be presenter-backed; this persona's list view is aggregate-only.
113
113
  - Be transparent about missing data and the reporting window. Keep the response concise, calm, and useful without hype.
114
114
 
115
115
  ## Four transparency asks
@@ -124,7 +124,7 @@ Be ready to answer:
124
124
  - Own context reads: read_user_contexts, preview_user_context, read_premises.
125
125
  - Own community context: read_networks, read_network_memberships.
126
126
  - Own pending-question reads: read_pending_questions (never answer them).
127
- - Aggregate activity reporting: report_agent_activity.
127
+ - Aggregate activity reporting: read_activity_summary.
128
128
  - Aggregate current opportunity reporting: list_opportunities (no counterpart identities or rows).${ctx.actionToolsEnabled ? "\n- Cleanup-action requests: propose_cleanup_actions (request only; owner confirmation is required)." : ""}
129
129
 
130
130
  ## Session identity (preloaded)
@@ -315,7 +315,6 @@ export const SIGNAL_PERSONA = {
315
315
  createTools: (deps, preResolvedContext) => createSignalTools(deps, preResolvedContext),
316
316
  loopBehaviors: {
317
317
  // Direct discovery is absent, so its create-intent retry callback must stay off.
318
- createIntentCallback: false,
319
318
  // create_intent can legitimately return proposal cards; retain recovery/stripping.
320
319
  hallucinationRecovery: true,
321
320
  },
@@ -3,7 +3,22 @@ import { requestContext } from "../../shared/observability/request-context.js";
3
3
  import { log } from "../../shared/observability/log.js";
4
4
  import { renderNetworkContext } from "../../shared/network/metadata.renderer.js";
5
5
  import { success, error, UUID_REGEX } from "../../shared/agent/tool.helpers.js";
6
+ import { focusedNetworkId } from "../../shared/agent/tool.scope.js";
6
7
  import { NetworkRecommender } from "./network.recommender.js";
8
+ /**
9
+ * Resolves the community this caller is hard-bound to, if any.
10
+ *
11
+ * A user-driven network-scoped chat carries `context.networkId`; a network-scoped
12
+ * agent (personal/external API key bound to one community over MCP) instead
13
+ * carries the focused scope envelope (`scopeType='network'`, `scopeId`) applied
14
+ * at the MCP boundary, with `networkId` left unset. Both must clamp community
15
+ * and roster reads/writes to the exact bound community BEFORE any graph/adapter
16
+ * work — not only via the scoped-deps data clamp — so a foreign community is
17
+ * denied with a stable message and never read or mutated (IND-591).
18
+ */
19
+ function boundCommunityId(context) {
20
+ return focusedNetworkId(context) ?? context.networkId;
21
+ }
7
22
  // Lazy singleton — only instantiated on first onboarding ranking call so that
8
23
  // importing this module does not require OPENROUTER_API_KEY at load time.
9
24
  let recommender;
@@ -54,7 +69,7 @@ export function createNetworkTools(defineTool, deps) {
54
69
  const readIndexes = defineTool({
55
70
  name: "read_networks",
56
71
  description: "Lists the authenticated user's networks (communities), including ones they own and public communities they can join.\n\n" +
57
- "**When to use:** To find network IDs for scoping other operations (read_intents, discover_opportunities, read_network_memberships), " +
72
+ "**When to use:** To find network IDs for scoping other operations (read_intents, list_opportunities, read_network_memberships), " +
58
73
  "or to show the user which communities they belong to.\n\n" +
59
74
  "**Returns:** Up to three lists — `memberOf` (networks the user joined), `owns` (networks the user created), and `publicNetworks` " +
60
75
  "(publicly joinable communities the user is not yet a member of). Entries in `memberOf` include `isPersonal` set to `true` for the user's " +
@@ -68,12 +83,13 @@ export function createNetworkTools(defineTool, deps) {
68
83
  if (query.userId && query.userId.trim() !== context.userId) {
69
84
  return error("You can only list your own networks. Omit userId to see the current user's networks.");
70
85
  }
86
+ const boundNetworkId = boundCommunityId(context);
71
87
  const _readGraphStart = Date.now();
72
88
  const _readTraceEmitter = requestContext.getStore()?.traceEmitter;
73
89
  _readTraceEmitter?.({ type: "graph_start", name: "index" });
74
90
  const result = await graphs.index.invoke({
75
91
  userId: context.userId,
76
- networkId: context.networkId || undefined,
92
+ networkId: boundNetworkId || undefined,
77
93
  operationMode: 'read',
78
94
  showAll: false, // Never allow bypass — strict scope enforcement
79
95
  });
@@ -91,12 +107,12 @@ export function createNetworkTools(defineTool, deps) {
91
107
  ...(Array.isArray(rr.publicNetworks) ? { publicNetworks: enrichWithContext(rr.publicNetworks) } : {}),
92
108
  };
93
109
  // When scoped, add clear metadata so model knows results are limited
94
- if (context.networkId) {
110
+ if (boundNetworkId) {
95
111
  return success({
96
112
  ...enriched,
97
113
  scopeRestriction: {
98
114
  isScoped: true,
99
- scopedToNetwork: context.indexName ?? context.networkId,
115
+ scopedToNetwork: context.indexName ?? boundNetworkId,
100
116
  message: `Results are limited to "${context.indexName ?? 'this network'}" because this chat is scoped to that community. The user may belong to other communities not shown here.`,
101
117
  },
102
118
  _graphTimings: [{ name: 'index', durationMs: _readGraphMs, agents: result.agentTimings ?? [] }],
@@ -192,13 +208,14 @@ export function createNetworkTools(defineTool, deps) {
192
208
  handler: async ({ context, query }) => {
193
209
  const networkId = query.networkId?.trim() || undefined;
194
210
  const userId = query.userId?.trim() || undefined;
211
+ const boundNetworkId = boundCommunityId(context);
195
212
  if (networkId && !UUID_REGEX.test(networkId)) {
196
213
  return error("Invalid network ID format. Use the exact UUID from read_networks.");
197
214
  }
198
215
  // Mode 1: list members of a network
199
216
  if (networkId && !userId) {
200
217
  // Enforce strict scope: when chat is network-scoped, only allow querying that network
201
- if (context.networkId && networkId !== context.networkId) {
218
+ if (boundNetworkId && networkId !== boundNetworkId) {
202
219
  return error(`This chat is scoped to ${context.indexName ?? 'this network'}. You can only query members of this network.`);
203
220
  }
204
221
  const _readMembersStart = Date.now();
@@ -226,7 +243,7 @@ export function createNetworkTools(defineTool, deps) {
226
243
  // Cross-user access: validate shared membership scope
227
244
  const callerMemberships = await userDb.getNetworkMemberships();
228
245
  if (networkId) {
229
- if (context.networkId && networkId !== context.networkId) {
246
+ if (boundNetworkId && networkId !== boundNetworkId) {
230
247
  return error(`This chat is scoped to ${context.indexName ?? 'this network'}. You can only query membership in this community.`);
231
248
  }
232
249
  const callerInNetwork = callerMemberships.some((m) => m.networkId === networkId);
@@ -241,16 +258,16 @@ export function createNetworkTools(defineTool, deps) {
241
258
  }
242
259
  else {
243
260
  // Strict scope enforcement: when chat is network-scoped, only check the scoped network
244
- if (context.networkId) {
245
- const isMember = await systemDb.isNetworkMember(context.networkId, targetUserId);
261
+ if (boundNetworkId) {
262
+ const isMember = await systemDb.isNetworkMember(boundNetworkId, targetUserId);
246
263
  if (isMember) {
247
264
  return success({
248
265
  isMember: true,
249
266
  userId: targetUserId,
250
- networkId: context.networkId,
267
+ networkId: boundNetworkId,
251
268
  scopeRestriction: {
252
269
  isScoped: true,
253
- scopedToNetwork: context.indexName ?? context.networkId,
270
+ scopedToNetwork: context.indexName ?? boundNetworkId,
254
271
  message: `This chat is scoped to "${context.indexName ?? 'this network'}". Only membership in this community is shown.`,
255
272
  },
256
273
  });
@@ -258,11 +275,11 @@ export function createNetworkTools(defineTool, deps) {
258
275
  return success({
259
276
  isMember: false,
260
277
  userId: targetUserId,
261
- networkId: context.networkId,
278
+ networkId: boundNetworkId,
262
279
  message: "User is not a member of this community.",
263
280
  scopeRestriction: {
264
281
  isScoped: true,
265
- scopedToNetwork: context.indexName ?? context.networkId,
282
+ scopedToNetwork: context.indexName ?? boundNetworkId,
266
283
  message: `This chat is scoped to "${context.indexName ?? 'this network'}". Only membership in this community was checked.`,
267
284
  },
268
285
  });
@@ -292,13 +309,13 @@ export function createNetworkTools(defineTool, deps) {
292
309
  // Own memberships — use userDb
293
310
  memberships = await userDb.getNetworkMemberships();
294
311
  // Strict scope: when chat is network-scoped, only return the scoped network membership
295
- if (context.networkId && !networkId) {
296
- memberships = memberships.filter((m) => m.networkId === context.networkId);
312
+ if (boundNetworkId && !networkId) {
313
+ memberships = memberships.filter((m) => m.networkId === boundNetworkId);
297
314
  }
298
315
  }
299
316
  // If both networkId and userId: filter to that specific membership
300
317
  if (networkId) {
301
- if (context.networkId && networkId !== context.networkId) {
318
+ if (boundNetworkId && networkId !== boundNetworkId) {
302
319
  return error(`This chat is scoped to ${context.indexName ?? 'this network'}. You can only query membership in this community.`);
303
320
  }
304
321
  const callerMemberships = await userDb.getNetworkMemberships();
@@ -321,7 +338,7 @@ export function createNetworkTools(defineTool, deps) {
321
338
  });
322
339
  }
323
340
  // Own memberships in scoped chat
324
- if (context.networkId && targetUserId === context.userId) {
341
+ if (boundNetworkId && targetUserId === context.userId) {
325
342
  return success({
326
343
  userId: targetUserId,
327
344
  count: memberships.length,
@@ -333,7 +350,7 @@ export function createNetworkTools(defineTool, deps) {
333
350
  })),
334
351
  scopeRestriction: {
335
352
  isScoped: true,
336
- scopedToNetwork: context.indexName ?? context.networkId,
353
+ scopedToNetwork: context.indexName ?? boundNetworkId,
337
354
  message: `Results are limited to "${context.indexName ?? 'this network'}" because this chat is scoped to that community. The user may belong to other communities not shown here.`,
338
355
  },
339
356
  });
@@ -372,11 +389,12 @@ export function createNetworkTools(defineTool, deps) {
372
389
  settings: updateNetworkSettingsSchema.describe("Object with fields to update. All fields are optional — only include the ones to change. title: display name. prompt: purpose description (used for intent auto-assignment). imageUrl: community image URL (null to remove). joinPolicy: 'anyone' or 'invite_only'. allowGuestVibeCheck: boolean."),
373
390
  }),
374
391
  handler: async ({ context, query }) => {
375
- const effectiveNetworkId = (query.networkId?.trim() || context.networkId) ?? null;
392
+ const boundNetworkId = boundCommunityId(context);
393
+ const effectiveNetworkId = (query.networkId?.trim() || boundNetworkId) ?? null;
376
394
  if (!effectiveNetworkId || !UUID_REGEX.test(effectiveNetworkId)) {
377
395
  return error("Valid networkId required.");
378
396
  }
379
- if (context.networkId && effectiveNetworkId !== context.networkId) {
397
+ if (boundNetworkId && effectiveNetworkId !== boundNetworkId) {
380
398
  return error(`This chat is scoped to ${context.indexName ?? 'this network'}. You can only update this community's settings.`);
381
399
  }
382
400
  const _updateStart = Date.now();
@@ -403,7 +421,7 @@ export function createNetworkTools(defineTool, deps) {
403
421
  "(complementary matches) between members. The network's prompt guides what kinds of intents belong.\n\n" +
404
422
  "**When to use:** When the user wants to create a new community — e.g. a professional network, interest group, or project team.\n\n" +
405
423
  "**Returns:** The new network's networkId (UUID) and title. Use the networkId to add members (create_network_membership), " +
406
- "link intents (create_intent_index), or run discovery (discover_opportunities with networkId).",
424
+ "link intents (create_intent_index). Approved signals are matched in the background; use list_opportunities only to review persisted opportunities.",
407
425
  querySchema: z.object({
408
426
  title: z.string().describe("Display name of the network (e.g. 'AI Founders Berlin', 'Design Co-op'). Required."),
409
427
  prompt: z.string().optional().describe("Description of what this community is about. Used by the system to evaluate which intents belong in this network. Highly recommended for better auto-assignment."),
@@ -455,11 +473,12 @@ export function createNetworkTools(defineTool, deps) {
455
473
  networkId: z.string().optional().describe("Network UUID to delete. Get from read_networks. Defaults to the scoped network in network-scoped chats. Cannot be a personal network."),
456
474
  }),
457
475
  handler: async ({ context, query }) => {
458
- const networkId = query.networkId?.trim() || context.networkId;
476
+ const boundNetworkId = boundCommunityId(context);
477
+ const networkId = query.networkId?.trim() || boundNetworkId;
459
478
  if (!networkId || !UUID_REGEX.test(networkId)) {
460
479
  return error("Valid networkId required.");
461
480
  }
462
- if (context.networkId && networkId !== context.networkId) {
481
+ if (boundNetworkId && networkId !== boundNetworkId) {
463
482
  return error(`This chat is scoped to ${context.indexName ?? 'this network'}. You can only delete this community.`);
464
483
  }
465
484
  const _deleteStart = Date.now();
@@ -493,12 +512,13 @@ export function createNetworkTools(defineTool, deps) {
493
512
  networkId: z.string().optional().describe("Network UUID to add the member to. Get from read_networks. Defaults to the scoped network in network-scoped chats."),
494
513
  }),
495
514
  handler: async ({ context, query }) => {
496
- const networkId = query.networkId?.trim() || context.networkId;
515
+ const boundNetworkId = boundCommunityId(context);
516
+ const networkId = query.networkId?.trim() || boundNetworkId;
497
517
  const targetUserId = query.userId?.trim() || context.userId;
498
518
  if (!networkId || !UUID_REGEX.test(networkId)) {
499
519
  return error("Invalid network ID format. Use the exact UUID from read_networks.");
500
520
  }
501
- if (context.networkId && networkId !== context.networkId) {
521
+ if (boundNetworkId && networkId !== boundNetworkId) {
502
522
  return error(`This chat is scoped to ${context.indexName ?? 'this network'}. You can only add members to this community.`);
503
523
  }
504
524
  const _createMemberStart = Date.now();
@@ -529,7 +549,7 @@ export function createNetworkTools(defineTool, deps) {
529
549
  const deleteNetworkMembership = defineTool({
530
550
  name: "delete_network_membership",
531
551
  description: "Removes a user from a network (community). After removal, the user's intents are unlinked from this network " +
532
- "and they can no longer participate in opportunity discovery within it.\n\n" +
552
+ "and their approved signals are no longer eligible for background matching within it.\n\n" +
533
553
  "**Permissions:** Only the network owner can remove members. The owner themselves cannot be removed (delete the network instead).\n\n" +
534
554
  "**When to use:** When a network owner wants to remove a member from the community. " +
535
555
  "Use read_network_memberships(networkId) first to get the userId of the member to remove.\n\n" +
@@ -539,7 +559,8 @@ export function createNetworkTools(defineTool, deps) {
539
559
  networkId: z.string().optional().describe("Network UUID to remove the member from. Get from read_networks. Defaults to the scoped network in network-scoped chats."),
540
560
  }),
541
561
  handler: async ({ context, query }) => {
542
- const networkId = query.networkId?.trim() || context.networkId;
562
+ const boundNetworkId = boundCommunityId(context);
563
+ const networkId = query.networkId?.trim() || boundNetworkId;
543
564
  const targetUserId = query.userId?.trim();
544
565
  if (!networkId || !UUID_REGEX.test(networkId)) {
545
566
  return error("Valid networkId required. Use the exact UUID from read_networks.");
@@ -547,7 +568,7 @@ export function createNetworkTools(defineTool, deps) {
547
568
  if (!targetUserId) {
548
569
  return error("userId is required.");
549
570
  }
550
- if (context.networkId && networkId !== context.networkId) {
571
+ if (boundNetworkId && networkId !== boundNetworkId) {
551
572
  return error(`This chat is scoped to ${context.indexName ?? 'this network'}. You can only manage members of this community.`);
552
573
  }
553
574
  const _deleteMemberStart = Date.now();