@opengeni/api-router 3.0.1 → 4.0.0

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 (96) hide show
  1. package/dist/admission-trace.d.ts +8 -0
  2. package/dist/app.js +1 -1
  3. package/dist/auth/managed-auth-attempt-context.d.ts +2 -0
  4. package/dist/auth/managed-auth-session-adapter.d.ts +2 -0
  5. package/dist/auth/managed-auth.d.ts +1 -0
  6. package/dist/auth/managed-sign-in-notifications.d.ts +4 -0
  7. package/dist/{chunk-AJ62T5QN.js → chunk-E7TZFJ65.js} +22300 -21113
  8. package/dist/{chunk-AJ62T5QN.js.map → chunk-E7TZFJ65.js.map} +1 -1
  9. package/dist/connection-ownership.d.ts +1 -2
  10. package/dist/fatal-process-boundary.d.ts +1 -1
  11. package/dist/index.js +24 -6
  12. package/dist/index.js.map +1 -1
  13. package/dist/integrations/atlassian.d.ts +8 -3
  14. package/dist/integrations/fiken.d.ts +3 -1
  15. package/dist/integrations/github-lens-connect.d.ts +5 -65
  16. package/dist/integrations/google-drive.d.ts +2 -0
  17. package/dist/integrations/integration-commit-authority.d.ts +13 -0
  18. package/dist/integrations/oauth-client.d.ts +119 -11
  19. package/dist/integrations/oauth-profiles.d.ts +8 -13
  20. package/dist/integrations/slack-interactions.d.ts +1 -1
  21. package/dist/mcp/capability-account-readiness.d.ts +7 -0
  22. package/dist/mcp/first-party-tool-permissions.d.ts +4 -0
  23. package/dist/mcp-oauth.d.ts +22 -1
  24. package/dist/routes/api-integrations.d.ts +1 -0
  25. package/dist/routes/codex.d.ts +28 -0
  26. package/dist/routes/managed-sign-in-methods.d.ts +6 -0
  27. package/dist/routes/organization-integration-policy.d.ts +3 -0
  28. package/dist/routes/sessions.d.ts +24 -2
  29. package/dist/scheduled-task-deletion.d.ts +3 -4
  30. package/dist/workspace-tool-gateway.d.ts +3 -1
  31. package/package.json +15 -15
  32. package/src/admission-trace.ts +14 -0
  33. package/src/app.ts +141 -94
  34. package/src/auth/managed-auth-attempt-context.ts +9 -0
  35. package/src/auth/managed-auth-session-adapter.ts +11 -0
  36. package/src/auth/managed-auth.ts +39 -2
  37. package/src/auth/managed-sign-in-notifications.ts +81 -0
  38. package/src/connection-ownership.ts +2 -9
  39. package/src/editable-artifact-production.ts +6 -0
  40. package/src/fatal-process-boundary.ts +17 -2
  41. package/src/index.ts +3 -4
  42. package/src/integrations/atlassian.ts +184 -76
  43. package/src/integrations/fiken.ts +57 -35
  44. package/src/integrations/github-app-connect.ts +51 -15
  45. package/src/integrations/github-lens-connect.ts +7 -28
  46. package/src/integrations/google-drive.ts +296 -125
  47. package/src/integrations/integration-commit-authority.ts +69 -0
  48. package/src/integrations/oauth-client.ts +231 -132
  49. package/src/integrations/oauth-profiles.ts +10 -45
  50. package/src/integrations/personal-github.ts +55 -31
  51. package/src/integrations/provider-oauth.ts +57 -82
  52. package/src/integrations/social-oauth.ts +83 -56
  53. package/src/mcp/capability-account-readiness.ts +17 -0
  54. package/src/mcp/editable-artifacts.ts +7 -2
  55. package/src/mcp/first-party-tool-permissions.ts +1 -0
  56. package/src/mcp/knowledge-entries.ts +25 -8
  57. package/src/mcp/server.ts +135 -43
  58. package/src/mcp-oauth.ts +336 -34
  59. package/src/routes/api-integrations.ts +116 -4
  60. package/src/routes/automations.ts +5 -13
  61. package/src/routes/billing.ts +52 -0
  62. package/src/routes/canonical-human-identities.ts +61 -0
  63. package/src/routes/capabilities.ts +51 -1
  64. package/src/routes/codex.ts +4 -2
  65. package/src/routes/connect.ts +90 -31
  66. package/src/routes/connections.ts +487 -307
  67. package/src/routes/files.ts +21 -12
  68. package/src/routes/github.ts +48 -19
  69. package/src/routes/insights.ts +17 -7
  70. package/src/routes/integration-facets.ts +66 -25
  71. package/src/routes/knowledge.ts +48 -1
  72. package/src/routes/managed-sign-in-methods.ts +433 -0
  73. package/src/routes/organization-integration-policy.ts +113 -0
  74. package/src/routes/organization-memberships.ts +22 -2
  75. package/src/routes/plugins.ts +42 -4
  76. package/src/routes/pr-review-github.ts +67 -57
  77. package/src/routes/pr-review.ts +74 -71
  78. package/src/routes/rigs.ts +32 -13
  79. package/src/routes/scheduled-tasks.ts +6 -6
  80. package/src/routes/sessions.ts +233 -11
  81. package/src/routes/skill-content.ts +19 -3
  82. package/src/routes/social.ts +24 -13
  83. package/src/sandbox/channel-a.ts +7 -1
  84. package/src/sandbox/rematerialize.ts +2 -1
  85. package/src/scheduled-task-deletion.ts +13 -3
  86. package/src/workspace-tool-gateway.ts +31 -31
  87. package/dist/connection-authority-owner.d.ts +0 -59
  88. package/dist/routes/connection-authorities.d.ts +0 -3
  89. package/dist/routes/host-mcp-bindings.d.ts +0 -5
  90. package/dist/routes/host-mcp-resolvers.d.ts +0 -3
  91. package/dist/routes/packs.d.ts +0 -3
  92. package/src/connection-authority-owner.ts +0 -61
  93. package/src/routes/connection-authorities.ts +0 -139
  94. package/src/routes/host-mcp-bindings.ts +0 -122
  95. package/src/routes/host-mcp-resolvers.ts +0 -41
  96. package/src/routes/packs.ts +0 -784
@@ -2,12 +2,14 @@ import {
2
2
  AgentInstructionSaveRequest,
3
3
  WorkspaceInstructionPolicyTarget,
4
4
  KnowledgeEntryListRequest,
5
+ KnowledgeSavePreparationRequest,
5
6
  KnowledgeEntrySaveRequest,
6
7
  KnowledgeTaskNotePromotionRequest,
7
8
  type AccessGrant,
8
9
  } from "@opengeni/contracts";
9
10
  import {
10
11
  prepareKnowledgeFile,
12
+ prepareKnowledgeSave,
11
13
  retainKnowledgeMessage,
12
14
  searchKnowledgeEntries,
13
15
  requireLiveAgentAttemptAuthorization,
@@ -131,11 +133,23 @@ export function registerKnowledgeEntryTools(
131
133
  throw new Error("Pass exactly one existing confirmation target");
132
134
  }),
133
135
  );
136
+ server.registerTool(
137
+ "knowledge_prepare_save",
138
+ {
139
+ description:
140
+ "Before saving useful lasting Knowledge, find related published entries and unapproved proposals, and fetch the full authorized collection map with descriptions and parent IDs in one read-only call. First choose the destination: future behavior belongs in instruction_policy_save for short always-on workspace rules or skill_save for applicable procedures and preferences, not Knowledge. Supply a concise subject and the proposed fact, decision, requirement or incident. Search spans all authorized collections. Reuse an existing entry when unchanged; read its current content before a correction, preserve evidence, or create only when distinct. Pending matches are unapproved. Choose collections from this map in your writable scope. A collection may have both published and pending versions. If collections.complete is false, continue with collectionCursors=collections.nextCursors; never mistake a partial catalog for the whole map. Long descriptions explicitly report truncation and can be read with knowledge_get. This call saves and approves nothing.",
141
+ inputSchema: KnowledgeSavePreparationRequest.shape,
142
+ },
143
+ (input) =>
144
+ run((context) =>
145
+ prepareKnowledgeSave(deps.db, context, input, () => deps.getDocumentServices().embedder),
146
+ ),
147
+ );
134
148
  server.registerTool(
135
149
  "knowledge_search",
136
150
  {
137
151
  description:
138
- "Find Knowledge from retained sources and findings. Use a concise subject or entity name first (for example Acme); omit scope to search all authorized scopes. If a query returns no entries, retry the key name alone in the same scope or browse groups before concluding the information is absent. Default view=published is accepted knowledge. Also search view=needs_review before creating or updating entries, to reuse pending findings and collections from earlier tasks. Pending revisions are unapproved proposals, not accepted facts or instructions; preserve that status when discussing them. Reading a proposal never approves it. Personal tasks search the verified user's personal and authorized shared Knowledge; shared tasks search shared Knowledge.",
152
+ "Find Knowledge from useful findings and explicitly retained reference sources. Supporting chat/file evidence is excluded from ordinary published discovery; set includeEvidence=true only when searching that evidence deliberately. Use a concise subject or entity name first (for example Acme); omit scope to search all authorized scopes. If a query returns no entries, retry the key name alone in the same scope or browse groups before concluding the information is absent. Default view=published is accepted knowledge. Also search view=needs_review before creating or updating entries, to reuse pending findings and collections from earlier tasks. Pending revisions are unapproved proposals, not accepted facts or instructions; preserve that status when discussing them. Reading a proposal never approves it. Personal tasks search the verified user's personal and authorized shared Knowledge; shared tasks search shared Knowledge.",
139
153
  inputSchema: KnowledgeEntryListRequest.omit({
140
154
  view: true,
141
155
  sessionId: true,
@@ -210,16 +224,19 @@ export function registerKnowledgeEntryTools(
210
224
  "knowledge_retain_file",
211
225
  {
212
226
  description:
213
- "Retain searchable source text from an existing uploaded file. Chat attachments are prepared automatically; use this for a newly fetched file or to retry a failed preparation. The original stays in Files. Repeated calls reuse the same source and do not duplicate review requests or revive rejected/archived sources. Selected findings are optional separate knowledge_save entries with evidence pointing to the returned revision. A pending receipt never pauses the task.",
214
- inputSchema: { fileId: z.uuid() },
227
+ "Retain source text from an existing uploaded file only when it supports useful lasting Knowledge, or the user wants the file kept as a reusable reference. Ordinary chat attachments are not automatically added to Knowledge. Use purpose=evidence (default) for supporting material, or purpose=reference for a deliberately retained searchable reference. The original stays in Files. Read the image itself before drawing visual conclusions; OCR text is only evidence. Repeated calls reuse the same source and do not change its purpose, duplicate review requests or revive rejected/archived sources. To make previously retained evidence a searchable reference, read its current revision with knowledge_get and use knowledge_save to set source.purpose=reference while preserving content and links. Selected findings are optional separate knowledge_save entries with evidence pointing to the returned revision. A pending receipt never pauses the task.",
228
+ inputSchema: {
229
+ fileId: z.uuid(),
230
+ purpose: z.enum(["evidence", "reference"]).default("evidence"),
231
+ },
215
232
  },
216
- ({ fileId }) => run((context) => prepareKnowledgeFile(deps, context, fileId)),
233
+ ({ fileId, purpose }) => run((context) => prepareKnowledgeFile(deps, context, fileId, purpose)),
217
234
  );
218
235
  server.registerTool(
219
236
  "knowledge_retain_message",
220
237
  {
221
238
  description:
222
- "Retain the exact text of a user message from this conversation as a source entry. Use when a user supplies or confirms a durable fact, before saving a finding based on it. Omit messageId for the user message that triggered this turn, or pass an earlier user message event ID from this same conversation. The server copies the actual message, preserves its identity and follows this task's learning policy. Repeated calls reuse the source. Cite the returned entryId/revisionId as evidence in knowledge_save, with location.messageIds=[messageId]. Retaining a message does not prove every statement in it or approve a pending finding.",
239
+ "Retain the exact text of a user message as supporting evidence only after identifying useful lasting Knowledge to save. Do not retain routine approvals, acknowledgments, task instructions or status chatter merely because they occurred. The source stays out of ordinary Knowledge discovery and can be read through its evidence link. Use when a user supplies or confirms a durable fact, before saving a finding based on it. Omit messageId for the user message that triggered this turn, or pass an earlier user message event ID from this same conversation. The server copies the actual message, preserves its identity and follows this task's learning policy. Repeated calls reuse the source. Cite the returned entryId/revisionId as evidence in knowledge_save, with location.messageIds=[messageId]. Retaining a message does not prove every statement in it or approve a pending finding.",
223
240
  inputSchema: { messageId: z.uuid().optional() },
224
241
  },
225
242
  ({ messageId }) => run((context) => retainKnowledgeMessage(deps.db, context, messageId)),
@@ -228,7 +245,7 @@ export function registerKnowledgeEntryTools(
228
245
  "knowledge_save",
229
246
  {
230
247
  description:
231
- "Retain useful source text, facts, decisions, requirements, incidents or notes, or organize them with groups and relationships. Do this autonomously when useful for future work. Choose kind by content: source for retained original text; fact for a specific claim (the label is not verification); decision for a choice made and its reasoning; requirement for a need or constraint; incident for a problem with cause, fix and outcome when known; note for other useful context. Choose the closest kind without asking the user to classify it. A group is a collection about a customer, product, system or subject, not a finding type. Search published and needs_review views for an existing entry and relevant group before creating one, use groupIds to link entries across sources, and reuse an entry in multiple groups instead of copying it. Nest collections by setting a group entry's groupIds to its parent collections; keep the hierarchy shallow and useful, and never create circular membership. Use a new entryId and expectedVersion 0 to create; use an existing ID and its current version to correct or reorganize. Source text and selected facts are independent entries, not mandatory duplicate stages. Evidence pins another entry's exact revision. When a user supplies or confirms a fact, retain the actual message with knowledge_retain_message and cite the returned revision; do not leave its provenance only in prose. Preserve existing evidence and relationships on corrections unless they are specifically no longer applicable; a contradictory original remains useful evidence of what changed. Use the same operationId only for an exact retry. Agent learning decides publication: published is available immediately; pending is saved for review and your task continues without an approval prompt. Do not turn Knowledge into instructions or Skills.",
248
+ "Retain retrieval-only facts, decisions, requirements, incidents or notes, or deliberately selected reference sources, not a behavioral preference or instruction for future sessions. Use instruction_policy_save for short always-on workspace rules and skill_save for applicable procedures and behavioral preferences; do not store them here as facts such as 'the user prefers concise replies', even if those destinations are unavailable or require review. First use knowledge_prepare_save when available (otherwise search published and needs_review Knowledge and browse collections). Skip unchanged duplicates; improve an existing entry when the subject is the same. Do not save routine approvals, acknowledgments, temporary instructions, screenshots or OCR merely because they occurred. State the reusable finding with enough context for a future task, preserve uncertainty and evidence, and place it in the relevant existing collections. Do this autonomously when useful for future work. Choose kind by content: source for retained original text; fact for a specific claim (the label is not verification); decision for a choice made and its reasoning; requirement for a need or constraint; incident for a problem with cause, fix and outcome when known; note for other useful context. Choose the closest kind without asking the user to classify it. A group is a collection about a customer, product, system or subject, not a finding type. Search published and needs_review views for an existing entry and relevant group before creating one, use groupIds to link entries across sources, and reuse an entry in multiple groups instead of copying it. Nest collections by setting a group entry's groupIds to its parent collections; keep the hierarchy shallow and useful, and never create circular membership. Use a new entryId and expectedVersion 0 to create; use an existing ID and its current version to correct or reorganize. Source text and selected facts are independent entries, not mandatory duplicate stages. Evidence pins another entry's exact revision. When a user supplies or confirms a fact, retain the actual message with knowledge_retain_message and cite the returned revision; do not leave its provenance only in prose. Preserve existing evidence and relationships on corrections unless they are specifically no longer applicable; a contradictory original remains useful evidence of what changed. Use the same operationId only for an exact retry. Agent learning decides publication: published is available immediately; pending is saved for review and your task continues without an approval prompt. Do not turn Knowledge into instructions or Skills.",
232
249
  inputSchema: KnowledgeEntrySaveRequest.omit({ scope: true }).shape,
233
250
  },
234
251
  (input) => run((context) => saveKnowledgeEntry(deps.db, context, input)),
@@ -259,7 +276,7 @@ export function registerKnowledgeEntryTools(
259
276
  "instruction_policy_save",
260
277
  {
261
278
  description:
262
- "Change a concise standing workspace instruction through this task's Agent learning policy. Read the current policy first and submit its exact baseline. Use editMode=append by default for a new rule; it preserves the current content and adds one blank-line separator. Use edit with a localized oldText that occurs exactly once and newText to update or remove only that passage. Agents cannot replace the complete instruction; direct the user to the manual workspace-instruction editor for a whole-policy rewrite. Each supplied text and the resulting instruction are limited to 600 characters. Facts and incidents belong in knowledge_save and reusable procedures in skill_save. Review first saves an inactive revision and returns pending; continue the task without an approval question.",
279
+ "Change a concise standing workspace instruction through this task's Agent learning policy. Use for short always-on behavior such as 'Keep replies concise; expand when asked', within the intended workspace scope; active instructions enter applicable prompts without Knowledge retrieval. Read the current policy first and submit its exact baseline. Use editMode=append by default for a new rule; it preserves the current content and adds one blank-line separator. Use edit with a localized oldText that occurs exactly once and newText to update or remove only that passage. Agents cannot replace the complete instruction; direct the user to the manual workspace-instruction editor for a whole-policy rewrite. Each supplied text and the resulting instruction are limited to 600 characters. Facts and incidents belong in knowledge_save and reusable procedures or context-specific preferences in skill_save. Preserve personal scope rather than creating a workspace-wide rule. Review first saves an inactive revision and returns pending; continue the task without an approval question. Report the actual receipt; Knowledge is not a workaround for Off, pending review, unavailable scope or size limits.",
263
280
  inputSchema: AgentInstructionSaveRequest.shape,
264
281
  },
265
282
  (input) => run((context) => saveAgentInstruction(deps.db, context, input), "instruction"),
@@ -268,7 +285,7 @@ export function registerKnowledgeEntryTools(
268
285
  "task_note_promote_knowledge",
269
286
  {
270
287
  description:
271
- "Retain one active task note as durable Knowledge with its exact text and origin. The note stays temporary and unchanged; this creates one Knowledge entry in this task's scope, governed by the same learning policy. Pending review never pauses the task. Use knowledge_save for a selected or rewritten finding instead.",
288
+ "Retain one active task note as durable Knowledge with its exact text and origin, only for retrieval-only information. Do not promote behavior rules or preferences to Knowledge; use instruction_policy_save for short always-on workspace rules or skill_save for applicable procedures and preferences, under their own scope and learning policy. The note stays temporary and unchanged; this creates one Knowledge entry in this task's scope, governed by the same learning policy. Pending review never pauses the task. Use knowledge_save for a selected or rewritten finding instead.",
272
289
  inputSchema: KnowledgeTaskNotePromotionRequest.shape,
273
290
  },
274
291
  (input) => run((context) => promoteTaskNoteToKnowledge(deps.db, context, input)),
package/src/mcp/server.ts CHANGED
@@ -9,6 +9,7 @@ import {
9
9
  setSessionChannel,
10
10
  } from "@opengeni/db";
11
11
  import { createHash, randomUUID } from "node:crypto";
12
+ import { capabilityAccountReadiness } from "./capability-account-readiness";
12
13
  import {
13
14
  prepareWorkspaceArtifactUpload,
14
15
  prepareWorkspaceArtifactPublication,
@@ -18,6 +19,8 @@ import {
18
19
  CreateScheduledTaskRequest,
19
20
  CreateSessionRequest,
20
21
  GoalSpec,
22
+ SessionGoalReportRequirements,
23
+ SessionGoalReportDeliveries,
21
24
  boundSessionMcpText as capSessionDiscoveryText,
22
25
  compactSessionMcpListRow,
23
26
  sessionMcpIncludesRelatedWork,
@@ -92,6 +95,7 @@ import {
92
95
  getSessionMcpMonitoringSummary,
93
96
  getSessionQueueSnapshot,
94
97
  getSessionTurn,
98
+ getSessionTurnMcpAccountBindings,
95
99
  getOrCreatePreferenceRegistrySnapshot,
96
100
  getPreferenceRegistryFullContent,
97
101
  getVariableSet,
@@ -160,6 +164,7 @@ import type { AnySchema, ZodRawShapeCompat } from "@modelcontextprotocol/sdk/ser
160
164
  import type { ToolAnnotations } from "@modelcontextprotocol/sdk/types.js";
161
165
  import { HTTPException } from "hono/http-exception";
162
166
  import * as z4 from "zod/v4";
167
+ import { editableArtifactActorForGrant } from "../routes/editable-artifacts";
163
168
  import { registerKnowledgeEntryTools } from "./knowledge-entries";
164
169
  import {
165
170
  FIRST_PARTY_TOOL_AUTHORIZATION,
@@ -230,6 +235,7 @@ import {
230
235
  validateScheduledTaskMachineTarget,
231
236
  validateScheduledTaskTarget,
232
237
  updateScheduledTaskForApi,
238
+ triggerScheduledTaskForGrant,
233
239
  validatedScheduledTaskUpdate,
234
240
  } from "@opengeni/core";
235
241
  import {
@@ -797,8 +803,7 @@ export function buildOpenGeniMcpServer(
797
803
  server.registerTool(
798
804
  "social_connections_list",
799
805
  {
800
- description:
801
- "List connected social media accounts available to social media analysis packs.",
806
+ description: "List connected social media accounts available for analysis.",
802
807
  inputSchema: { limit: z4.number().int().positive().optional() },
803
808
  },
804
809
  async ({ limit }) =>
@@ -1001,7 +1006,7 @@ export function buildOpenGeniMcpServer(
1001
1006
 
1002
1007
  // Provider-scoped aliases are the canonical tools advertised by the X and
1003
1008
  // Reddit Integration cards. The legacy social_* names remain available to
1004
- // existing Packs/sessions, but these names bind the provider in the tool
1009
+
1005
1010
  // identity and reject a near-identical Connection from the other adapter.
1006
1011
  for (const provider of ["x", "reddit"] as const) {
1007
1012
  const providerName = provider === "x" ? "X" : "Reddit";
@@ -1379,7 +1384,7 @@ export function buildOpenGeniMcpServer(
1379
1384
  status: z4.string().optional(),
1380
1385
  // Explicit credential-free connection authority selections; declared
1381
1386
  // so MCP validation doesn't strip them before the contract parse.
1382
- connectionAuthorities: z4.array(z4.unknown()).optional(),
1387
+ connectionAccounts: z4.array(z4.unknown()).optional(),
1383
1388
  variableSetId: z4.string().uuid().optional(),
1384
1389
  // Deprecated alias of variableSetId; declared so MCP validation doesn't
1385
1390
  // strip it before the contract parse maps it (rename back-compat).
@@ -1446,7 +1451,7 @@ export function buildOpenGeniMcpServer(
1446
1451
  status: z4.string().optional(),
1447
1452
  // Omitted preserves the frozen selections, [] clears them, and an
1448
1453
  // array replaces them; declared so MCP validation doesn't strip it.
1449
- connectionAuthorities: z4.array(z4.unknown()).optional(),
1454
+ connectionAccounts: z4.array(z4.unknown()).optional(),
1450
1455
  variableSetId: z4.string().uuid().nullable().optional(),
1451
1456
  // Deprecated alias of variableSetId (rename back-compat); declared so MCP
1452
1457
  // validation doesn't strip it before the contract parse maps it.
@@ -1475,7 +1480,7 @@ export function buildOpenGeniMcpServer(
1475
1480
  if (!scheduledTaskUpdateChangesState(existing, update)) {
1476
1481
  return json(scheduledTaskReceipt("scheduled_tasks_update", existing, "unchanged", false));
1477
1482
  }
1478
- const task = await updateScheduledTaskForApi(deps.db, grant.workspaceId, id, update);
1483
+ const task = await updateScheduledTaskForApi(deps.db, grant, id, update);
1479
1484
  await syncUpdatedScheduledTask({
1480
1485
  db: deps.db,
1481
1486
  workflowClient: deps.workflowClient,
@@ -1498,7 +1503,7 @@ export function buildOpenGeniMcpServer(
1498
1503
  return json(scheduledTaskReceipt("scheduled_tasks_pause", existing, "unchanged", false));
1499
1504
  }
1500
1505
  const previous = await captureScheduledTaskRestoreState(deps.db, existing);
1501
- const task = await updateScheduledTaskForApi(deps.db, grant.workspaceId, id, {
1506
+ const task = await updateScheduledTaskForApi(deps.db, grant, id, {
1502
1507
  status: "paused",
1503
1508
  });
1504
1509
  await syncUpdatedScheduledTask({
@@ -1533,7 +1538,7 @@ export function buildOpenGeniMcpServer(
1533
1538
  sessionAuthorization: deps.sessionAuthorization,
1534
1539
  authorizationSurface: "first_party_mcp",
1535
1540
  });
1536
- const task = await updateScheduledTaskForApi(deps.db, grant.workspaceId, id, update);
1541
+ const task = await updateScheduledTaskForApi(deps.db, grant, id, update);
1537
1542
  await syncUpdatedScheduledTask({
1538
1543
  db: deps.db,
1539
1544
  workflowClient: deps.workflowClient,
@@ -1599,7 +1604,7 @@ export function buildOpenGeniMcpServer(
1599
1604
  ? manualScheduledTaskTriggerUsageKey(grant.workspaceId, task.id, triggerToken)
1600
1605
  : `knowledge-source-sync:manual:${grant.workspaceId}:${task.id}:${triggerToken}`;
1601
1606
  const triggerWorkflowId = manualScheduledTaskTriggerWorkflowId(task.id, triggerToken);
1602
- await deps.workflowClient.triggerScheduledTask({
1607
+ await triggerScheduledTaskForGrant(deps.db, grant, deps.workflowClient, {
1603
1608
  task,
1604
1609
  agentRunUsageIdempotencyKey,
1605
1610
  triggerWorkflowId,
@@ -1654,9 +1659,8 @@ export function buildOpenGeniMcpServer(
1654
1659
  },
1655
1660
  async ({ id }) => {
1656
1661
  const { task, changed } = await deleteScheduledTaskWithDurableCleanup(deps, {
1657
- workspaceId: grant.workspaceId,
1662
+ grant,
1658
1663
  taskId: id,
1659
- subjectId: grant.subjectId,
1660
1664
  });
1661
1665
  return json(
1662
1666
  mcpMutationReceipt({
@@ -2254,7 +2258,20 @@ function registerAtlassianTools(
2254
2258
  const authority = candidates[0]!;
2255
2259
  const metadata = AtlassianConnectionMetadata.safeParse(authority.connection.metadata);
2256
2260
  if (!metadata.success) throw new Error("Atlassian connection metadata is invalid");
2261
+ const claims = exactAgentAttemptClaims(grant);
2262
+ if (grant.principalKind === "agent_attempt" && !claims) {
2263
+ throw new Error("Atlassian access requires the exact active agent attempt");
2264
+ }
2257
2265
  return {
2266
+ ...(claims
2267
+ ? {
2268
+ connectionUseContext: {
2269
+ ...claims,
2270
+ accountId: grant.accountId,
2271
+ workspaceId: grant.workspaceId,
2272
+ },
2273
+ }
2274
+ : {}),
2258
2275
  connection: authority.connection,
2259
2276
  metadata: metadata.data,
2260
2277
  subjectId: authority.subjectId ?? grant.subjectId,
@@ -2271,7 +2288,10 @@ function registerAtlassianTools(
2271
2288
  async ({ connectionId }) => {
2272
2289
  const authority = await connectionFor(connectionId);
2273
2290
  const response = await browseAtlassianSources(deps, {
2274
- workspaceId: grant.workspaceId,
2291
+ workspaceId: authority.connection.workspaceId,
2292
+ ...(authority.connectionUseContext
2293
+ ? { connectionUseContext: authority.connectionUseContext }
2294
+ : {}),
2275
2295
  subjectId: authority.subjectId,
2276
2296
  connectionId: authority.connection.id,
2277
2297
  });
@@ -2304,7 +2324,10 @@ function registerAtlassianTools(
2304
2324
  return json({
2305
2325
  connectionId: authority.connection.id,
2306
2326
  results: await searchAtlassianLive(deps, {
2307
- workspaceId: grant.workspaceId,
2327
+ workspaceId: authority.connection.workspaceId,
2328
+ ...(authority.connectionUseContext
2329
+ ? { connectionUseContext: authority.connectionUseContext }
2330
+ : {}),
2308
2331
  subjectId: authority.subjectId,
2309
2332
  connectionId: authority.connection.id,
2310
2333
  query,
@@ -2330,7 +2353,10 @@ function registerAtlassianTools(
2330
2353
  const authority = await connectionFor(connectionId);
2331
2354
  return json(
2332
2355
  await getAtlassianLiveItem(deps, {
2333
- workspaceId: grant.workspaceId,
2356
+ workspaceId: authority.connection.workspaceId,
2357
+ ...(authority.connectionUseContext
2358
+ ? { connectionUseContext: authority.connectionUseContext }
2359
+ : {}),
2334
2360
  subjectId: authority.subjectId,
2335
2361
  connectionId: authority.connection.id,
2336
2362
  kind,
@@ -2420,7 +2446,7 @@ function registerGoalTools(
2420
2446
  "goal_set",
2421
2447
  {
2422
2448
  description:
2423
- "Create a goal when this session has none, or replace a completed goal with a new one. While active, idle moments synthesize continuation turns until goal_complete or goal_pause. To change an active or paused goal, use goal_update with its objective revision, a change kind, and rationale.",
2449
+ "Create a goal when this session has none, or replace a completed goal with a new one. Declare user-facing native document reports with reportRequirements before producing them. While active, idle moments synthesize continuation turns until goal_complete or goal_pause. To change an active or paused goal, use goal_update with its objective revision, a change kind, and rationale.",
2424
2450
  // `maxAutoContinuations` is deliberately not agent-facing: the ceiling is
2425
2451
  // API/scheduled-task pacing configuration, and an agent that set its own
2426
2452
  // cap used to silence its orchestration for hours. Continuation pacing is
@@ -2428,9 +2454,10 @@ function registerGoalTools(
2428
2454
  inputSchema: {
2429
2455
  text: goalText,
2430
2456
  successCriteria: successCriteriaSchema.optional(),
2457
+ reportRequirements: SessionGoalReportRequirements.optional(),
2431
2458
  },
2432
2459
  },
2433
- async ({ text, successCriteria }) => {
2460
+ async ({ text, successCriteria, reportRequirements }) => {
2434
2461
  await authorizeFirstPartySession(deps, grant, sessionId, "session.goal.write");
2435
2462
  await requireSession(deps.db, grant.workspaceId, sessionId);
2436
2463
  const existing = await getSessionGoal(deps.db, grant.workspaceId, sessionId);
@@ -2444,15 +2471,24 @@ function registerGoalTools(
2444
2471
  ? (grant.metadata["turnId"] as string)
2445
2472
  : null;
2446
2473
  await assertGoalReactivationAllowed(deps, grant.workspaceId, sessionId, callerTurnId);
2474
+ const context = exactAgentCommandContext(grant, sessionId);
2447
2475
  const { goal, replaced, events } = await upsertSessionGoalWithEvent(deps.db, {
2448
2476
  accountId: grant.accountId,
2449
2477
  workspaceId: grant.workspaceId,
2450
2478
  sessionId,
2451
2479
  text,
2452
2480
  successCriteria: successCriteria ?? null,
2481
+ ...(reportRequirements !== undefined ? { reportRequirements } : {}),
2453
2482
  maxAutoContinuations: null,
2454
2483
  createdBy: "agent",
2455
2484
  actor: "agent",
2485
+ commandActor: {
2486
+ type: "agent_attempt",
2487
+ sessionId: context.callerSessionId,
2488
+ turnId: context.callerTurnId,
2489
+ attemptId: context.callerAttemptId,
2490
+ executionGeneration: context.callerExecutionGeneration,
2491
+ },
2456
2492
  });
2457
2493
  if (events.length > 0) {
2458
2494
  await deps.bus.publish(grant.workspaceId, sessionId, events);
@@ -2540,13 +2576,14 @@ function registerGoalTools(
2540
2576
  "goal_progress",
2541
2577
  {
2542
2578
  description:
2543
- "Record concrete progress toward the unchanged active goal. This does not change goal text, success criteria, mutation policy, or objective revision. Do not use it merely to keep the continuation loop alive.",
2579
+ "Record concrete progress toward the unchanged active goal. Optionally append reportRequirements for secondary user-facing reports discovered during other work; existing requirement IDs and titles cannot be changed or removed. This does not change goal text, success criteria, mutation policy, or objective revision. Do not use it merely to keep the continuation loop alive.",
2544
2580
  inputSchema: {
2545
2581
  progressNote: progressNoteSchema,
2546
2582
  idempotencyKey: z4.string().uuid(),
2583
+ reportRequirements: SessionGoalReportRequirements.optional(),
2547
2584
  },
2548
2585
  },
2549
- async ({ progressNote, idempotencyKey }) => {
2586
+ async ({ progressNote, idempotencyKey, reportRequirements }) => {
2550
2587
  await authorizeFirstPartySession(deps, grant, sessionId, "session.goal.write");
2551
2588
  const context = exactAgentCommandContext(grant, sessionId);
2552
2589
  const { goal, events, operationId, replay } = await recordSessionGoalProgressWithEvent(
@@ -2555,6 +2592,7 @@ function registerGoalTools(
2555
2592
  sessionId,
2556
2593
  {
2557
2594
  progressNote,
2595
+ ...(reportRequirements !== undefined ? { reportRequirements } : {}),
2558
2596
  command: {
2559
2597
  accountId: grant.accountId,
2560
2598
  actor: {
@@ -2635,16 +2673,23 @@ function registerGoalTools(
2635
2673
  "goal_complete",
2636
2674
  {
2637
2675
  description:
2638
- "Mark the session goal as completed. Requires concrete evidence (what was done and how it satisfies the success criteria). Completion prevents further continuation turns.",
2639
- inputSchema: { evidence: z4.string().min(1) },
2676
+ "Mark the session goal as completed with concrete evidence. Every persisted report requirement must have a matching reportDeliveries entry containing a native document artifactId and its server-issued inspectionReceiptId from a post-edit body inspection. Missing, stale, inaccessible or summary-only proof fails; inspect again after an edit. Omit reportDeliveries only when no reports were declared. Successful completion returns report artifact references and prevents further continuation turns.",
2677
+ inputSchema: {
2678
+ evidence: z4.string().min(1),
2679
+ reportDeliveries: SessionGoalReportDeliveries.optional(),
2680
+ },
2640
2681
  },
2641
- async ({ evidence }) => {
2682
+ async ({ evidence, reportDeliveries }) => {
2642
2683
  await authorizeFirstPartySession(deps, grant, sessionId, "session.goal.write");
2643
2684
  await requireSession(deps.db, grant.workspaceId, sessionId);
2644
2685
  const existing = await getSessionGoal(deps.db, grant.workspaceId, sessionId);
2645
2686
  if (!existing) {
2646
2687
  throw new Error("this session has no goal; use goal_set first");
2647
2688
  }
2689
+ const context = exactAgentCommandContext(grant, sessionId);
2690
+ const reportArtifactActor = editableArtifactActorForGrant(grant, "0000000000000001");
2691
+ if (reportArtifactActor.kind !== "agent")
2692
+ throw new Error("Goal completion requires exact agent authority");
2648
2693
  const { goal, events } = await setSessionGoalStatusWithEvent(
2649
2694
  deps.db,
2650
2695
  grant.workspaceId,
@@ -2652,6 +2697,15 @@ function registerGoalTools(
2652
2697
  {
2653
2698
  status: "completed",
2654
2699
  evidence,
2700
+ ...(reportDeliveries !== undefined ? { reportDeliveries } : {}),
2701
+ reportArtifactActor,
2702
+ commandActor: {
2703
+ type: "agent_attempt",
2704
+ sessionId: context.callerSessionId,
2705
+ turnId: context.callerTurnId,
2706
+ attemptId: context.callerAttemptId,
2707
+ executionGeneration: context.callerExecutionGeneration,
2708
+ },
2655
2709
  event: { type: "goal.completed", evidence },
2656
2710
  },
2657
2711
  );
@@ -2659,8 +2713,8 @@ function registerGoalTools(
2659
2713
  if (events.length > 0) {
2660
2714
  await deps.bus.publish(grant.workspaceId, sessionId, events);
2661
2715
  }
2662
- return json(
2663
- mcpMutationReceipt({
2716
+ return json({
2717
+ ...mcpMutationReceipt({
2664
2718
  operation: "goal_complete",
2665
2719
  committed: true,
2666
2720
  outcome: changed ? "updated" : "unchanged",
@@ -2675,7 +2729,13 @@ function registerGoalTools(
2675
2729
  idempotency: { status: "not_supported" },
2676
2730
  nextAction: { tool: "session_get", arguments: { sessionId } },
2677
2731
  }),
2678
- );
2732
+ reportDeliveries: SessionGoalReportDeliveries.parse(
2733
+ goal.metadata.reportDeliveriesV1 ?? [],
2734
+ ).map((delivery) => ({
2735
+ ...delivery,
2736
+ artifactReference: `[Open report](/workspaces/${grant.workspaceId}/artifacts/editable/${delivery.artifactId})`,
2737
+ })),
2738
+ });
2679
2739
  },
2680
2740
  );
2681
2741
 
@@ -3559,10 +3619,6 @@ function scheduledTaskUpdateChangesState(
3559
3619
  if (update.metadata !== undefined && stableJson(update.metadata) !== stableJson(task.metadata)) {
3560
3620
  return true;
3561
3621
  }
3562
- // Personal-connection delegations are recomputed with agentConfig and can
3563
- // change even when the visible config is byte-identical (for example after a
3564
- // connection rotation), so preserve that refresh as a real mutation.
3565
- if (update.personalConnectionDelegations !== undefined) return true;
3566
3622
  return false;
3567
3623
  }
3568
3624
 
@@ -3843,7 +3899,7 @@ function registerRigTools(
3843
3899
  const rig = await requireRigForApi(deps.db, await resourceGrant(), rigId);
3844
3900
  if (rig.scope === "organization") {
3845
3901
  throw new Error(
3846
- "Organization rig mutation requires account-admin authority through the authenticated REST surface.",
3902
+ "Organization sandbox environment mutation requires account-admin authority through the authenticated REST surface.",
3847
3903
  );
3848
3904
  }
3849
3905
  return rig;
@@ -3852,7 +3908,7 @@ function registerRigTools(
3852
3908
  server.registerTool(
3853
3909
  "rig_list",
3854
3910
  {
3855
- description: "List workspace rigs and their active versions.",
3911
+ description: "List workspace sandbox environments and their active versions.",
3856
3912
  inputSchema: {},
3857
3913
  },
3858
3914
  async () => json({ rigs: await listRigs(deps.db, await resourceGrant()) }),
@@ -3862,7 +3918,7 @@ function registerRigTools(
3862
3918
  "rig_get",
3863
3919
  {
3864
3920
  description:
3865
- "Get one rig's bounded active definition plus compact historical version/change summaries. Historical setup scripts, checks, payloads, and verification logs are represented by counts/byte facts rather than copied into model context; use the access-controlled REST detail endpoints for exact retained definitions.",
3921
+ "Get one sandbox environment's bounded active definition plus compact historical version/change summaries. Historical setup scripts, checks, payloads, and verification logs are represented by counts/byte facts rather than copied into model context; use the access-controlled REST detail endpoints for exact retained definitions.",
3866
3922
  inputSchema: {
3867
3923
  rigId: z4.string().uuid(),
3868
3924
  versionLimit: z4.number().int().positive().optional(),
@@ -3893,7 +3949,7 @@ function registerRigTools(
3893
3949
  "rig_propose_change",
3894
3950
  {
3895
3951
  description:
3896
- "Propose an additive rig setup command for clean verification. Use the exact command that already worked in this sandbox.",
3952
+ "Propose an additive sandbox environment setup command for clean verification. Use the exact command that already worked in this sandbox.",
3897
3953
  inputSchema: {
3898
3954
  rigId: z4.string().uuid(),
3899
3955
  command: z4.string().min(1).max(8192),
@@ -3942,7 +3998,7 @@ function registerRigTools(
3942
3998
  retryable: true,
3943
3999
  },
3944
4000
  warnings: [
3945
- "The rig change and verifying transition committed, but verification workflow start failed.",
4001
+ "The sandbox environment change and verifying transition committed, but verification workflow start failed.",
3946
4002
  ],
3947
4003
  facts: { verificationAttempt: attempt },
3948
4004
  nextAction: { tool: "rig_get", arguments: { rigId: rig.id } },
@@ -3975,7 +4031,7 @@ function registerRigTools(
3975
4031
  "rig_verify",
3976
4032
  {
3977
4033
  description:
3978
- "Trigger rig verification. Pass changeId for a proposed change, or omit it to re-verify the active version's checks.",
4034
+ "Trigger sandbox environment verification. Pass changeId for a proposed change, or omit it to re-verify the active version's checks.",
3979
4035
  inputSchema: {
3980
4036
  rigId: z4.string().uuid(),
3981
4037
  changeId: z4.string().uuid().optional(),
@@ -4045,7 +4101,7 @@ function registerRigTools(
4045
4101
  );
4046
4102
  }
4047
4103
  if (!rig.activeVersion) {
4048
- throw new Error("rig has no active version");
4104
+ throw new Error("sandbox environment has no active version");
4049
4105
  }
4050
4106
  await deps.workflowClient.startRigVerification({
4051
4107
  workspaceId: rig.workspaceId,
@@ -4078,7 +4134,7 @@ function registerRigTools(
4078
4134
  "rig_promote",
4079
4135
  {
4080
4136
  description:
4081
- "Promote a verified definition_edit rig change to a new active immutable version. Requires rigs:manage.",
4137
+ "Promote a verified definition_edit sandbox environment change to a new active immutable version. Requires rigs:manage.",
4082
4138
  inputSchema: {
4083
4139
  rigId: z4.string().uuid(),
4084
4140
  changeId: z4.string().uuid(),
@@ -4885,7 +4941,7 @@ function registerWorkspaceOrchestrationTools(
4885
4941
  "session_create",
4886
4942
  {
4887
4943
  description:
4888
- "Spawn a new agent session (a worker) only for a concrete, bounded subtask that can run independently and has a defined integration point in your current work. Do not delegate work you will also perform yourself; track the child and join its actual result before completing dependent work. Give the child a concise semantic title; if omitted, OpenGeni derives one from its delegated goal or initial message. The child inherits this session's visibility, agent-access scope and end-user label; a private session can only create a same-owner private child, and memoryScope may only narrow this session's selector. Give a goal-bearing child its delegated objective. Its goal.rootConstraints may be an exact applicable subset of this accepted turn's frozen root constraints; omit that field to inherit all of them. Omit sandbox for the safe default: compatible children share the creator's box, while a different Variable Set, Rig, or machineTarget gets its own box. Use 'new' for deliberate isolation or {groupId} for a strict compatible sibling join. Put targetSandboxId and its optional workingDir together inside machineTarget; a machineTarget is always an own-box create even when the parent is backend none. To create a non-delegating leaf, pass a narrowed firstPartyMcpTools list that omits session_create; do not use a child-local depth override. Public REST/SDK callers retain advanced absolute depth and explicit shared-placement controls.",
4944
+ "Spawn a new agent session (a worker) only for a concrete, bounded subtask that can run independently and has a defined integration point in your current work. Do not delegate work you will also perform yourself; track the child and join its actual result before completing dependent work. Give the child a concise semantic title; if omitted, OpenGeni derives one from its delegated goal or initial message. The child inherits this session's visibility, agent-access scope and end-user label; a private session can only create a same-owner private child, and memoryScope may only narrow this session's selector. Give a goal-bearing child its delegated objective. Its goal.rootConstraints may be an exact applicable subset of this accepted turn's frozen root constraints; omit that field to inherit all of them. Omit sandbox for the safe default: compatible children share the creator's box, while a different Variable Set, Sandbox Environment, or machineTarget gets its own box. Use 'new' for deliberate isolation or {groupId} for a strict compatible sibling join. Put targetSandboxId and its optional workingDir together inside machineTarget; a machineTarget is always an own-box create even when the parent is backend none. To create a non-delegating leaf, pass a narrowed firstPartyMcpTools list that omits session_create; do not use a child-local depth override. Public REST/SDK callers retain advanced absolute depth and explicit shared-placement controls.",
4889
4945
  inputSchema: sessionCreateInput,
4890
4946
  },
4891
4947
  async (args) => {
@@ -5724,7 +5780,9 @@ function registerCapabilityDiscoveryTools(
5724
5780
  // This is a readiness projection, never credential resolution or a new grant.
5725
5781
  const setupProjections = async (items: CapabilityCatalogItem[]) => {
5726
5782
  const availableServerIds = new Set<string>();
5727
- if (items.some((item) => item.enabled && item.connectionRef?.subjectScope === "subject")) {
5783
+ const acceptedServerIds = new Set<string>();
5784
+ let accountSelectionKnown = false;
5785
+ if (items.some((item) => item.enabled && item.connectionRef)) {
5728
5786
  const claims = exactAgentCommandContext(grant, sessionId);
5729
5787
  const attemptCatalog = await getAttemptToolCatalog(deps.db, {
5730
5788
  accountId: grant.accountId,
@@ -5737,11 +5795,28 @@ function registerCapabilityDiscoveryTools(
5737
5795
  attemptCatalog.executionGeneration === claims.callerExecutionGeneration
5738
5796
  ) {
5739
5797
  for (const entry of attemptCatalog.entries) availableServerIds.add(entry.identity.serverId);
5798
+ const bindings = await getSessionTurnMcpAccountBindings(
5799
+ deps.db,
5800
+ grant.workspaceId,
5801
+ sessionId,
5802
+ claims.callerTurnId,
5803
+ );
5804
+ accountSelectionKnown = bindings !== null;
5805
+ const readiness = capabilityAccountReadiness(availableServerIds, bindings);
5806
+ for (const id of readiness.available) availableServerIds.add(id);
5807
+ for (const id of readiness.accepted) acceptedServerIds.add(id);
5740
5808
  }
5741
5809
  }
5742
5810
  return await Promise.all(
5743
5811
  items.map((item) =>
5744
- capabilitySetupProjection(deps, grant.workspaceId, item, availableServerIds),
5812
+ capabilitySetupProjection(
5813
+ deps,
5814
+ grant.workspaceId,
5815
+ item,
5816
+ availableServerIds,
5817
+ acceptedServerIds,
5818
+ accountSelectionKnown,
5819
+ ),
5745
5820
  ),
5746
5821
  );
5747
5822
  };
@@ -5750,7 +5825,7 @@ function registerCapabilityDiscoveryTools(
5750
5825
  "capability_catalog_search",
5751
5826
  {
5752
5827
  description:
5753
- "Search OpenGeni's reviewed workspace capability catalog when the user asks to add an integration or the task needs a capability that is not currently usable. Search by the outcome needed (for example `GitHub repositories`, `product analytics`, or `Slack notifications`), compare the returned candidates, and prefer a ready or verified exact match. This only reads secret-free metadata; it never installs, connects, or authorizes anything.",
5828
+ "Find integrations in OpenGeni's reviewed workspace catalog when the user asks to add one or needed access is missing. Search by integration name or task outcome. Results describe setup status and provide setup.nextAction when human setup can be requested. Use available tools directly for ready candidates. This reads metadata only and does not connect or authorize anything.",
5754
5829
  inputSchema: {
5755
5830
  query: z4.string().min(1).max(500),
5756
5831
  limit: z4.number().int().min(1).max(20).optional(),
@@ -5780,6 +5855,10 @@ function registerCapabilityDiscoveryTools(
5780
5855
  setup: {
5781
5856
  ...setups[index]!,
5782
5857
  requiredVariables: capabilityRequiredVariables(item),
5858
+ nextAction:
5859
+ setups[index]!.status === "authorization_required"
5860
+ ? { toolName: "capability_authorization_request", capabilityId: item.id }
5861
+ : null,
5783
5862
  },
5784
5863
  }));
5785
5864
  return json({ query, matches });
@@ -5790,7 +5869,7 @@ function registerCapabilityDiscoveryTools(
5790
5869
  "capability_authorization_request",
5791
5870
  {
5792
5871
  description:
5793
- "After capability_catalog_search and after explaining one chosen recommendation to the user, post exactly one in-session human authorization card for that catalog capability. This tool never grants access, enables a capability, reads a secret, or mints provider credentials; the authenticated user must click and confirm the provider/domain flow. Do not call it for a candidate reported ready or unavailable.",
5872
+ "Show a Connect card in this chat for a suitable capability returned by capability_catalog_search with setup.nextAction. Supply its capability ID and a brief rationale explaining how it helps the task; no separate confirmation is needed before showing the card. Requesting setup needs no integration-management permission and grants no access. The authenticated human completes setup through the card; never ask them to paste credentials into chat. Do not request another card for the same pending setup, or for a candidate reported ready or unavailable.",
5794
5873
  inputSchema: {
5795
5874
  capabilityId: z4.string().min(1).max(512),
5796
5875
  rationale: z4.string().min(1).max(2000),
@@ -5869,6 +5948,8 @@ async function capabilitySetupProjection(
5869
5948
  workspaceId: string,
5870
5949
  item: CapabilityCatalogItem,
5871
5950
  availableServerIds: ReadonlySet<string>,
5951
+ acceptedServerIds: ReadonlySet<string>,
5952
+ accountSelectionKnown: boolean,
5872
5953
  ): Promise<CapabilitySetupProjection> {
5873
5954
  if (item.id === "api:github-app" || item.surfaceType === "first_party_github") {
5874
5955
  const missing = githubAppMissingSettings(deps.settings);
@@ -5909,7 +5990,7 @@ async function capabilitySetupProjection(
5909
5990
  detail: "A workspace admin must designate an authorized Codex Apps subscription.",
5910
5991
  };
5911
5992
  }
5912
- if (item.enabled && item.connectionRef?.subjectScope === "subject") {
5993
+ if (item.enabled && item.connectionRef) {
5913
5994
  if (item.runtime.mcpServerId && availableServerIds.has(item.runtime.mcpServerId)) {
5914
5995
  return {
5915
5996
  status: "ready",
@@ -5917,11 +5998,22 @@ async function capabilitySetupProjection(
5917
5998
  detail: "This capability has tools available in this turn.",
5918
5999
  };
5919
6000
  }
6001
+ if (
6002
+ !accountSelectionKnown ||
6003
+ (item.runtime.mcpServerId && acceptedServerIds.has(item.runtime.mcpServerId))
6004
+ ) {
6005
+ return {
6006
+ status: "unavailable",
6007
+ action: null,
6008
+ detail:
6009
+ "This integration's tools are unavailable in this execution. Check its connection or setup failure before requesting reconnection; missing tools alone do not establish an authorization failure.",
6010
+ };
6011
+ }
5920
6012
  return {
5921
6013
  status: "authorization_required",
5922
6014
  action: "connect",
5923
6015
  detail:
5924
- "This capability is enabled, but its personal account is not available in this turn. The account owner must review access using Use in this conversation, then send a new message. Shared conversations require acknowledgement that results are visible to workspace members.",
6016
+ "This integration is enabled in the workspace, but no account was selected for this execution. Connect or select an account available to the sender, then send a new message.",
5925
6017
  };
5926
6018
  }
5927
6019
  if (item.enabled) {