@opengeni/api-router 0.30.3 → 2.2.0-canary.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 (165) hide show
  1. package/dist/api-websocket.d.ts +18 -0
  2. package/dist/app.js +1 -1
  3. package/dist/auth/managed-auth.d.ts +6 -0
  4. package/dist/{chunk-WCXHI3FF.js → chunk-3TP54PPX.js} +26692 -15389
  5. package/dist/chunk-3TP54PPX.js.map +1 -0
  6. package/dist/codemode.d.ts +8 -1
  7. package/dist/company-brain-okf.d.ts +24 -0
  8. package/dist/connected-machine-computer-access.d.ts +11 -0
  9. package/dist/connection-authority-owner.d.ts +59 -0
  10. package/dist/connection-ownership.d.ts +47 -0
  11. package/dist/controller-data-plane.d.ts +16 -0
  12. package/dist/editable-artifact-websocket.d.ts +5 -15
  13. package/dist/editable-artifact-workspace-files.d.ts +1 -1
  14. package/dist/http/api-error.d.ts +10 -0
  15. package/dist/http/cors.d.ts +5 -0
  16. package/dist/http/interaction-control-error.d.ts +13 -0
  17. package/dist/http/sse.d.ts +2 -3
  18. package/dist/index.d.ts +5 -2
  19. package/dist/index.js +326 -82
  20. package/dist/index.js.map +1 -1
  21. package/dist/integrations/atlassian.d.ts +17 -0
  22. package/dist/integrations/fiken.d.ts +7 -2
  23. package/dist/integrations/google-drive.d.ts +18 -0
  24. package/dist/integrations/oauth-client.d.ts +22 -3
  25. package/dist/integrations/oauth-profiles.d.ts +195 -0
  26. package/dist/integrations/personal-github-repositories.d.ts +45 -0
  27. package/dist/integrations/personal-github.d.ts +40 -0
  28. package/dist/integrations/pr-review-provider.d.ts +22 -0
  29. package/dist/integrations/provider-oauth.d.ts +8 -0
  30. package/dist/integrations/slack-app-home.d.ts +24 -0
  31. package/dist/integrations/slack-bot.d.ts +157 -9
  32. package/dist/integrations/slack-interactions.d.ts +39 -5
  33. package/dist/integrations/slack-routing.d.ts +120 -0
  34. package/dist/integrations/social-oauth.d.ts +5 -0
  35. package/dist/interaction-frame-proxy.d.ts +68 -0
  36. package/dist/mcp/company-brain-governed-writes.d.ts +26 -0
  37. package/dist/mcp/company-profile-agent-admin.d.ts +44 -0
  38. package/dist/mcp/documents.d.ts +1 -0
  39. package/dist/mcp/remember.d.ts +28 -0
  40. package/dist/mcp/request-abort.d.ts +19 -0
  41. package/dist/mcp/scheduled-task-view.d.ts +4 -4
  42. package/dist/mcp/server.d.ts +15 -2
  43. package/dist/mcp/session-view.d.ts +4 -0
  44. package/dist/mcp/session-wait.d.ts +137 -0
  45. package/dist/routes/automations.d.ts +4 -0
  46. package/dist/routes/billing.d.ts +5 -0
  47. package/dist/routes/browser-sessions.d.ts +4 -1
  48. package/dist/routes/company-brain.d.ts +3 -0
  49. package/dist/routes/company-profile.d.ts +2 -1
  50. package/dist/routes/connection-authorities.d.ts +3 -0
  51. package/dist/routes/personal-github-git-broker.d.ts +29 -0
  52. package/dist/routes/personal-github.d.ts +3 -0
  53. package/dist/routes/pr-review.d.ts +3 -0
  54. package/dist/routes/sessions.d.ts +1 -0
  55. package/dist/routes/user-resource-authorities.d.ts +3 -0
  56. package/dist/routes/workspace-capture.d.ts +10 -2
  57. package/dist/routes/workspace-learning.d.ts +3 -0
  58. package/dist/routes/workspaces.d.ts +19 -0
  59. package/dist/sandbox/channel-a.d.ts +11 -1
  60. package/dist/sandbox/connection-authority.d.ts +3 -0
  61. package/dist/sandbox/enrollment.d.ts +2 -0
  62. package/dist/sandbox/machines.d.ts +2 -2
  63. package/dist/sandbox/metrics-ingestion.d.ts +10 -20
  64. package/dist/sandbox/viewer.d.ts +24 -1
  65. package/dist/sandbox-file-artifacts.d.ts +11 -0
  66. package/dist/scheduled-task-deletion.d.ts +14 -0
  67. package/dist/slack-reaction-files.d.ts +1 -1
  68. package/dist/temporal-schedule-cleanup.d.ts +1 -0
  69. package/package.json +20 -18
  70. package/src/api-websocket.ts +23 -0
  71. package/src/app.ts +161 -23
  72. package/src/auth/managed-auth.ts +25 -3
  73. package/src/codemode.ts +53 -23
  74. package/src/codex-realtime.ts +8 -2
  75. package/src/company-brain-okf.ts +340 -0
  76. package/src/connected-machine-computer-access.ts +33 -0
  77. package/src/connection-authority-owner.ts +61 -0
  78. package/src/connection-ownership.ts +180 -0
  79. package/src/controller-data-plane.ts +47 -0
  80. package/src/editable-artifact-native-kernel.ts +26 -15
  81. package/src/editable-artifact-office-import.ts +28 -3
  82. package/src/editable-artifact-production.ts +13 -7
  83. package/src/editable-artifact-websocket.ts +11 -18
  84. package/src/editable-artifact-workspace-files.ts +31 -6
  85. package/src/http/api-error.ts +28 -0
  86. package/src/http/auth.ts +4 -0
  87. package/src/http/cors.ts +35 -0
  88. package/src/http/interaction-control-error.ts +164 -0
  89. package/src/http/sse.ts +112 -66
  90. package/src/index.ts +46 -6
  91. package/src/integrations/atlassian.ts +146 -35
  92. package/src/integrations/fiken.ts +102 -22
  93. package/src/integrations/google-drive.ts +301 -92
  94. package/src/integrations/oauth-client.ts +272 -143
  95. package/src/integrations/oauth-profiles.ts +477 -0
  96. package/src/integrations/personal-github-repositories.ts +445 -0
  97. package/src/integrations/personal-github.ts +705 -0
  98. package/src/integrations/pr-review-provider.ts +246 -0
  99. package/src/integrations/provider-oauth.ts +121 -5
  100. package/src/integrations/slack-app-home.ts +300 -0
  101. package/src/integrations/slack-bot.ts +342 -57
  102. package/src/integrations/slack-interactions.ts +1898 -259
  103. package/src/integrations/slack-routing.ts +337 -0
  104. package/src/integrations/social-oauth.ts +25 -0
  105. package/src/interaction-frame-proxy.ts +409 -0
  106. package/src/mcp/company-brain-governed-writes.ts +262 -0
  107. package/src/mcp/company-profile-agent-admin.ts +205 -0
  108. package/src/mcp/documents.ts +37 -6
  109. package/src/mcp/files.ts +16 -1
  110. package/src/mcp/remember.ts +181 -0
  111. package/src/mcp/request-abort.ts +44 -0
  112. package/src/mcp/scheduled-task-view.ts +1 -0
  113. package/src/mcp/server.ts +719 -369
  114. package/src/mcp/session-view.ts +54 -0
  115. package/src/mcp/session-wait.ts +554 -0
  116. package/src/model-catalog.ts +20 -12
  117. package/src/routes/api-integrations.ts +4 -0
  118. package/src/routes/api-keys.ts +1 -0
  119. package/src/routes/automations.ts +534 -0
  120. package/src/routes/billing.ts +57 -1
  121. package/src/routes/browser-sessions.ts +329 -79
  122. package/src/routes/channels.ts +17 -1
  123. package/src/routes/codex.ts +50 -0
  124. package/src/routes/company-brain.ts +367 -0
  125. package/src/routes/company-profile.ts +1 -1
  126. package/src/routes/computer-sessions.ts +320 -93
  127. package/src/routes/connection-authorities.ts +139 -0
  128. package/src/routes/connections.ts +144 -34
  129. package/src/routes/documents.ts +353 -4
  130. package/src/routes/editable-artifacts.ts +11 -3
  131. package/src/routes/enrollments.ts +116 -26
  132. package/src/routes/environments.ts +127 -55
  133. package/src/routes/files.ts +72 -11
  134. package/src/routes/install.ts +82 -28
  135. package/src/routes/integration-facets.ts +29 -0
  136. package/src/routes/interaction-resources.ts +20 -2
  137. package/src/routes/machines.ts +275 -13
  138. package/src/routes/organization-memberships.ts +717 -28
  139. package/src/routes/packs.ts +4 -3
  140. package/src/routes/personal-github-git-broker.ts +785 -0
  141. package/src/routes/personal-github.ts +319 -0
  142. package/src/routes/pr-review.ts +531 -0
  143. package/src/routes/rigs.ts +104 -39
  144. package/src/routes/scheduled-tasks.ts +19 -21
  145. package/src/routes/sessions.ts +775 -38
  146. package/src/routes/social.ts +14 -2
  147. package/src/routes/supergrok.ts +23 -4
  148. package/src/routes/transcription-recordings.ts +8 -2
  149. package/src/routes/user-resource-authorities.ts +138 -0
  150. package/src/routes/workspace-artifacts.ts +4 -1
  151. package/src/routes/workspace-capture.ts +21 -0
  152. package/src/routes/workspace-learning.ts +228 -0
  153. package/src/routes/workspaces.ts +59 -6
  154. package/src/sandbox/auth-callout.ts +38 -12
  155. package/src/sandbox/channel-a.ts +242 -19
  156. package/src/sandbox/connection-authority.ts +3 -0
  157. package/src/sandbox/enrollment.ts +15 -3
  158. package/src/sandbox/machines.ts +186 -64
  159. package/src/sandbox/metrics-ingestion.ts +220 -58
  160. package/src/sandbox/viewer.ts +243 -19
  161. package/src/sandbox-file-artifacts.ts +329 -0
  162. package/src/scheduled-task-deletion.ts +127 -0
  163. package/src/slack-reaction-files.ts +16 -5
  164. package/src/temporal-schedule-cleanup.ts +13 -0
  165. package/dist/chunk-WCXHI3FF.js.map +0 -1
@@ -0,0 +1,205 @@
1
+ import {
2
+ AGENT_AUTHORED_COMPANY_PROFILE_CONTENT_MAX_UTF8_BYTES,
3
+ AGENT_AUTHORED_COMPANY_PROFILE_ENTRY_MAX_CHARS,
4
+ AGENT_AUTHORED_COMPANY_PROFILE_SCALAR_MAX_CHARS,
5
+ AGENT_AUTHORED_COMPANY_PROFILE_TOO_LONG_MESSAGE,
6
+ AgentAuthoredCompanyProfileContent,
7
+ COMPANY_PROFILE_ENTRY_MAX_COUNT,
8
+ COMPANY_PROFILE_REASON_MAX_CHARS,
9
+ COMPANY_PROFILE_STABLE_KEY_MAX_CHARS,
10
+ normalizeCompanyProfileStableKey,
11
+ type CompanyProfileAgentAttempt,
12
+ type CompanyProfileEntry,
13
+ } from "@opengeni/contracts";
14
+ import {
15
+ CompanyProfileAgentAdminError,
16
+ createCompanyProfileAgentAdminRouter,
17
+ } from "@opengeni/core";
18
+ import type { Database } from "@opengeni/db";
19
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
20
+ import * as z from "zod/v4";
21
+
22
+ type JsonResult = (value: unknown) => {
23
+ content: { type: "text"; text: string }[];
24
+ };
25
+
26
+ export type RegisterCompanyProfileAgentAdminToolsInput = {
27
+ server: McpServer;
28
+ db: Database;
29
+ attempt: CompanyProfileAgentAttempt;
30
+ authorize: () => Promise<void>;
31
+ json: JsonResult;
32
+ router?: Pick<ReturnType<typeof createCompanyProfileAgentAdminRouter>, "propose" | "confirm">;
33
+ };
34
+
35
+ // Agent-only bounds. The human `account:admin` route keeps the wider
36
+ // `COMPANY_PROFILE_*` limits; this profile is the largest always-on prompt
37
+ // surface in the product, so an agent gets a much smaller budget for it.
38
+ const scalar = z
39
+ .string()
40
+ .trim()
41
+ .min(1)
42
+ .max(
43
+ AGENT_AUTHORED_COMPANY_PROFILE_SCALAR_MAX_CHARS,
44
+ AGENT_AUTHORED_COMPANY_PROFILE_TOO_LONG_MESSAGE,
45
+ )
46
+ .nullable();
47
+ const entry = z.object({
48
+ key: z.string().trim().min(1).max(COMPANY_PROFILE_STABLE_KEY_MAX_CHARS).optional(),
49
+ content: z
50
+ .string()
51
+ .trim()
52
+ .min(1)
53
+ .max(
54
+ AGENT_AUTHORED_COMPANY_PROFILE_ENTRY_MAX_CHARS,
55
+ AGENT_AUTHORED_COMPANY_PROFILE_TOO_LONG_MESSAGE,
56
+ ),
57
+ });
58
+ const entries = z.array(entry).max(COMPANY_PROFILE_ENTRY_MAX_COUNT);
59
+ const DEFAULT_PROPOSAL_REASON = "Activate agent-proposed organization company profile";
60
+ const STABLE_KEY_WORDS = 6;
61
+
62
+ type EntryInput = z.infer<typeof entry>;
63
+
64
+ /**
65
+ * Derive a stable key from entry content: the first few words, normalized into
66
+ * the company-profile key alphabet and bounded to the stable-key length.
67
+ */
68
+ export function deriveCompanyProfileStableKey(content: string): string {
69
+ const words = content
70
+ .normalize("NFKC")
71
+ .toLowerCase()
72
+ .replace(/[^\p{L}\p{N}\s._-]+/gu, " ")
73
+ .trim()
74
+ .split(/\s+/u)
75
+ .filter(Boolean)
76
+ .slice(0, STABLE_KEY_WORDS)
77
+ .join(" ");
78
+ const normalized = normalizeCompanyProfileStableKey(words)
79
+ .replace(/[^a-z0-9._-]+/g, "-")
80
+ .replace(/-+/g, "-")
81
+ .slice(0, COMPANY_PROFILE_STABLE_KEY_MAX_CHARS)
82
+ .replace(/^[._-]+|[._-]+$/g, "");
83
+ return normalized.length > 0 ? normalized : "entry";
84
+ }
85
+
86
+ /**
87
+ * Resolve explicit or derived keys for one list and de-duplicate by suffixing
88
+ * `-2`, `-3`, ... so the profile passes the unique-key contract.
89
+ */
90
+ export function resolveCompanyProfileEntries(input: readonly EntryInput[]): CompanyProfileEntry[] {
91
+ const used = new Set<string>();
92
+ return input.map(({ key, content }) => {
93
+ const base = key
94
+ ? normalizeCompanyProfileStableKey(key)
95
+ : deriveCompanyProfileStableKey(content);
96
+ let candidate = base;
97
+ for (let suffix = 2; used.has(candidate); suffix += 1) {
98
+ const tail = `-${suffix}`;
99
+ candidate = `${base.slice(0, COMPANY_PROFILE_STABLE_KEY_MAX_CHARS - tail.length)}${tail}`;
100
+ }
101
+ used.add(candidate);
102
+ return { key: candidate, content };
103
+ });
104
+ }
105
+
106
+ function boundedIssueMessage(error: z.ZodError): string {
107
+ return error.issues
108
+ .slice(0, 5)
109
+ .map((issue) => `${issue.path.join(".") || "profile"}: ${issue.message}`)
110
+ .join("; ")
111
+ .slice(0, 1_024);
112
+ }
113
+
114
+ /**
115
+ * Register the explicit owner-confirmed organization profile path. This is
116
+ * administration, not derived learning: workspace learning mode is never
117
+ * consulted, the initiating human must be the organization's active owner (the
118
+ * same authority as the manual `account:admin` route), and the proposal cannot
119
+ * become active without the exact bound human-input response returned by the
120
+ * first call.
121
+ */
122
+ export function registerCompanyProfileAgentAdminTools(
123
+ input: RegisterCompanyProfileAgentAdminToolsInput,
124
+ ): void {
125
+ const router = input.router ?? createCompanyProfileAgentAdminRouter({ db: input.db });
126
+ input.server.registerTool(
127
+ "company_profile_propose",
128
+ {
129
+ description:
130
+ "Prepare one complete organization company profile covering identity, mission, products, customers, strategic goals, and critical constraints. Omitted list keys are derived from content. " +
131
+ "Once activated, every field here is mandatory prompt context in every session for the whole organization, so write it as a concise profile rather than a document: one plain descriptive statement per field, no numbered procedure and no marketing copy. Longer source material belongs in organization Documents and is retrieved as evidence; never copy it into the profile. " +
132
+ `Bounds for this tool are ${AGENT_AUTHORED_COMPANY_PROFILE_SCALAR_MAX_CHARS} characters for identity and mission, ${AGENT_AUTHORED_COMPANY_PROFILE_ENTRY_MAX_CHARS} per list entry, ${COMPANY_PROFILE_ENTRY_MAX_COUNT} entries per list, and ${AGENT_AUTHORED_COMPANY_PROFILE_CONTENT_MAX_UTF8_BYTES} UTF-8 bytes for the whole profile. ` +
133
+ "This creates only an immutable inactive proposal for the exact live turn initiated by the organization owner and does not use workspace learning policy. The receipt returns the exact `humanInput` payload; call `request_human_input` with it verbatim, then call `company_profile_confirm` with the returned requestId.",
134
+ inputSchema: {
135
+ operationId: z.string().uuid(),
136
+ identity: scalar,
137
+ mission: scalar,
138
+ products: entries,
139
+ customers: entries,
140
+ goals: entries,
141
+ constraints: entries,
142
+ reason: z.string().trim().min(1).max(COMPANY_PROFILE_REASON_MAX_CHARS).optional(),
143
+ },
144
+ },
145
+ async (request) => {
146
+ await input.authorize();
147
+ const parsed = AgentAuthoredCompanyProfileContent.safeParse({
148
+ identity: request.identity,
149
+ mission: request.mission,
150
+ products: resolveCompanyProfileEntries(request.products),
151
+ customers: resolveCompanyProfileEntries(request.customers),
152
+ goals: resolveCompanyProfileEntries(request.goals),
153
+ constraints: resolveCompanyProfileEntries(request.constraints),
154
+ });
155
+ if (!parsed.success) {
156
+ return input.json({
157
+ status: "not_proposed",
158
+ code: "invalid_profile",
159
+ message: boundedIssueMessage(parsed.error),
160
+ });
161
+ }
162
+ try {
163
+ return input.json(
164
+ await router.propose({
165
+ attempt: input.attempt,
166
+ request: {
167
+ operationId: request.operationId,
168
+ profile: parsed.data,
169
+ reason: request.reason ?? DEFAULT_PROPOSAL_REASON,
170
+ },
171
+ }),
172
+ );
173
+ } catch (error) {
174
+ if (error instanceof CompanyProfileAgentAdminError) {
175
+ return input.json({ status: "not_proposed", code: error.code, message: error.message });
176
+ }
177
+ throw error;
178
+ }
179
+ },
180
+ );
181
+
182
+ input.server.registerTool(
183
+ "company_profile_confirm",
184
+ {
185
+ description:
186
+ "Activate a `company_profile_propose` receipt only after the exact initiating organization owner answered Activate on the returned `request_human_input` prompt. Pass the proposalReceiptId and requestId unchanged. Confirmation revalidates the live turn, organization role, proposal hash, and unchanged active-profile baseline, then returns the immutable activation receipt.",
187
+ inputSchema: {
188
+ operationId: z.string().uuid(),
189
+ proposalReceiptId: z.string().uuid(),
190
+ humanInputRequestId: z.string().uuid(),
191
+ },
192
+ },
193
+ async (request) => {
194
+ await input.authorize();
195
+ try {
196
+ return input.json(await router.confirm({ attempt: input.attempt, request }));
197
+ } catch (error) {
198
+ if (error instanceof CompanyProfileAgentAdminError) {
199
+ return input.json({ status: "not_confirmed", code: error.code, message: error.message });
200
+ }
201
+ throw error;
202
+ }
203
+ },
204
+ );
205
+ }
@@ -12,6 +12,7 @@ import {
12
12
  getDocumentChunk,
13
13
  listDocumentBases,
14
14
  listEffectiveIndexedDocuments,
15
+ resolveEffectiveDocumentAccess,
15
16
  searchEffectiveKnowledge,
16
17
  searchEffectiveDocuments,
17
18
  type DocumentAccessFilter,
@@ -60,6 +61,7 @@ export function buildDocumentsMcpServer(
60
61
  documentServices: DocumentServices,
61
62
  options: {
62
63
  createdBySessionId?: string | undefined;
64
+ attemptId?: string | undefined;
63
65
  /** Immutable human subject whose agent is making this retrieval request. */
64
66
  initiatingSubjectId: string;
65
67
  },
@@ -71,10 +73,18 @@ export function buildDocumentsMcpServer(
71
73
  // This server is the agent retrieval surface. Agent-disabled documents are
72
74
  // never reachable. Workspace-visible documents are shared; private
73
75
  // documents are available only to the creating subject's agent.
74
- const agentAccess: DocumentAccessFilter = {
75
- agentOnly: true,
76
- viewerSubjectId: options.initiatingSubjectId,
77
- };
76
+ const agentAuthority =
77
+ options.createdBySessionId && options.attemptId
78
+ ? { sessionId: options.createdBySessionId, attemptId: options.attemptId }
79
+ : undefined;
80
+ const resolveAgentAccess = async (): Promise<DocumentAccessFilter> =>
81
+ await resolveEffectiveDocumentAccess(db, {
82
+ accountId,
83
+ workspaceId,
84
+ initiatingSubjectId: options.initiatingSubjectId,
85
+ surface: "agent",
86
+ agentAuthority,
87
+ });
78
88
 
79
89
  server.registerTool(
80
90
  "list_document_bases",
@@ -101,6 +111,7 @@ export function buildDocumentsMcpServer(
101
111
  documentServices,
102
112
  input,
103
113
  options.initiatingSubjectId,
114
+ agentAuthority,
104
115
  ),
105
116
  );
106
117
 
@@ -119,6 +130,7 @@ export function buildDocumentsMcpServer(
119
130
  documentServices,
120
131
  input,
121
132
  options.initiatingSubjectId,
133
+ agentAuthority,
122
134
  ),
123
135
  );
124
136
 
@@ -134,6 +146,7 @@ export function buildDocumentsMcpServer(
134
146
  accountId,
135
147
  workspaceId,
136
148
  initiatingSubjectId: options.initiatingSubjectId,
149
+ agentAuthority,
137
150
  id,
138
151
  });
139
152
  return {
@@ -173,6 +186,7 @@ export function buildDocumentsMcpServer(
173
186
  accountId,
174
187
  workspaceId,
175
188
  initiatingSubjectId: options.initiatingSubjectId,
189
+ agentAuthority,
176
190
  ...(parentId ? { parentId } : {}),
177
191
  ...(topic ? { topic } : {}),
178
192
  ...(sourceKinds ? { sourceKinds } : {}),
@@ -206,6 +220,7 @@ export function buildDocumentsMcpServer(
206
220
  accountId,
207
221
  workspaceId,
208
222
  initiatingSubjectId: options.initiatingSubjectId,
223
+ agentAuthority,
209
224
  ...(checkpoint ? { checkpoint } : {}),
210
225
  ...(limit ? { limit } : {}),
211
226
  }),
@@ -225,7 +240,13 @@ export function buildDocumentsMcpServer(
225
240
  },
226
241
  },
227
242
  async ({ chunkId }) => {
228
- const found = await getDocumentChunk(db, accountId, workspaceId, chunkId, agentAccess);
243
+ const found = await getDocumentChunk(
244
+ db,
245
+ accountId,
246
+ workspaceId,
247
+ chunkId,
248
+ await resolveAgentAccess(),
249
+ );
229
250
  return {
230
251
  content: [
231
252
  { type: "text", text: found ? JSON.stringify(found) : `chunk not found: ${chunkId}` },
@@ -244,7 +265,13 @@ export function buildDocumentsMcpServer(
244
265
  },
245
266
  },
246
267
  async ({ chunkId }) => {
247
- const found = await getDocumentChunk(db, accountId, workspaceId, chunkId, agentAccess);
268
+ const found = await getDocumentChunk(
269
+ db,
270
+ accountId,
271
+ workspaceId,
272
+ chunkId,
273
+ await resolveAgentAccess(),
274
+ );
248
275
  return {
249
276
  content: [
250
277
  { type: "text", text: found ? JSON.stringify(found) : `chunk not found: ${chunkId}` },
@@ -367,6 +394,7 @@ async function searchContent(
367
394
  aclTags?: string[] | undefined;
368
395
  },
369
396
  initiatingSubjectId: string,
397
+ agentAuthority: { sessionId: string; attemptId: string } | undefined,
370
398
  ) {
371
399
  const results = await searchEffectiveDocuments(
372
400
  db,
@@ -380,6 +408,7 @@ async function searchContent(
380
408
  ...(input.sourceKinds ? { sourceKinds: input.sourceKinds } : {}),
381
409
  ...(input.aclTags ? { aclTags: input.aclTags } : {}),
382
410
  initiatingSubjectId,
411
+ agentAuthority,
383
412
  surface: "agent",
384
413
  },
385
414
  documentServices,
@@ -408,6 +437,7 @@ async function searchKnowledge(
408
437
  aclTags?: string[] | undefined;
409
438
  },
410
439
  initiatingSubjectId: string,
440
+ agentAuthority: { sessionId: string; attemptId: string } | undefined,
411
441
  ) {
412
442
  const response = await searchEffectiveKnowledge(
413
443
  db,
@@ -421,6 +451,7 @@ async function searchKnowledge(
421
451
  ...(input.sourceKinds ? { sourceKinds: input.sourceKinds } : {}),
422
452
  ...(input.aclTags ? { aclTags: input.aclTags } : {}),
423
453
  initiatingSubjectId,
454
+ agentAuthority,
424
455
  surface: "agent",
425
456
  },
426
457
  documentServices,
package/src/mcp/files.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { AccessGrant } from "@opengeni/contracts";
2
- import { requireFileForSubject } from "@opengeni/db";
2
+ import { recordAuditEvent, requireFileForSubject } from "@opengeni/db";
3
3
  import { hasPermission, type ApiRouteDeps } from "@opengeni/core";
4
4
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
5
5
  import * as z from "zod/v4";
@@ -47,6 +47,21 @@ export function buildFilesMcpServer(deps: ApiRouteDeps, grant: AccessGrant): Mcp
47
47
  throw new Error(`file is ${file.status}`);
48
48
  }
49
49
  const signed = await deps.objectStorage.createGetUrl({ key: file.objectKey });
50
+ // Principal-facing signed URL issuance is a metadata-only audit
51
+ // fact, awaited before the URL leaves the platform. Never the URL/key.
52
+ await recordAuditEvent(deps.db, {
53
+ accountId: grant.accountId,
54
+ workspaceId: grant.workspaceId,
55
+ subjectId: grant.subjectId,
56
+ action: "file.signed_url.issued",
57
+ targetType: "workspace_file",
58
+ targetId: file.id,
59
+ metadata: {
60
+ fileId: file.id,
61
+ kind: "mcp_download",
62
+ expiresAt: signed.expiresAt.toISOString(),
63
+ },
64
+ });
50
65
  return {
51
66
  content: [
52
67
  {
@@ -0,0 +1,181 @@
1
+ import {
2
+ AGENT_AUTHORED_INSTRUCTION_POLICY_CONTENT_MAX_CHARS,
3
+ AGENT_AUTHORED_PREFERENCE_CONTENT_MAX_CHARS,
4
+ AGENT_AUTHORED_REMEMBER_CONTENT_TOO_LONG_MESSAGE,
5
+ PREFERENCE_REGISTRY_DESCRIPTOR_DESCRIPTION_MAX_CHARS,
6
+ PREFERENCE_REGISTRY_TITLE_MAX_CHARS,
7
+ REMEMBER_CONTENT_MAX_CHARS,
8
+ WorkspaceInstructionPolicyTarget,
9
+ agentAuthoredDurableTextTooLongMessage,
10
+ type CompanyBrainGovernedWriteAttempt,
11
+ type RememberLane,
12
+ } from "@opengeni/contracts";
13
+ import { RememberError, createRememberRouter } from "@opengeni/core";
14
+ import type { Database } from "@opengeni/db";
15
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
16
+ import * as z from "zod/v4";
17
+
18
+ type JsonResult = (value: unknown) => {
19
+ content: { type: "text"; text: string }[];
20
+ };
21
+
22
+ export type RegisterRememberToolsInput = {
23
+ server: McpServer;
24
+ db: Database;
25
+ attempt: CompanyBrainGovernedWriteAttempt;
26
+ authorize: () => Promise<void>;
27
+ json: JsonResult;
28
+ router?: Pick<ReturnType<typeof createRememberRouter>, "remember" | "confirm">;
29
+ };
30
+
31
+ /**
32
+ * Per-lane budgets. The flat MCP input schema cannot vary `content` by lane, so
33
+ * it carries the widest lane (Knowledge) and the handler rejects an over-budget
34
+ * prompt-composed lane with the actionable message before anything durable is
35
+ * written. The contract enforces the same bounds for every other caller.
36
+ */
37
+ const laneContentMaxChars: Record<RememberLane, number> = {
38
+ instruction_policy: AGENT_AUTHORED_INSTRUCTION_POLICY_CONTENT_MAX_CHARS,
39
+ preference: AGENT_AUTHORED_PREFERENCE_CONTENT_MAX_CHARS,
40
+ knowledge: REMEMBER_CONTENT_MAX_CHARS,
41
+ };
42
+
43
+ const laneFields = {
44
+ operationId: z.string().uuid(),
45
+ content: z
46
+ .string()
47
+ .trim()
48
+ .min(1)
49
+ // The widest lane, because one flat schema serves all three. Carry the
50
+ // lane-aware guidance in the message so an over-long rule is not refused
51
+ // with a generic bound that points at the wrong number.
52
+ .max(REMEMBER_CONTENT_MAX_CHARS, AGENT_AUTHORED_REMEMBER_CONTENT_TOO_LONG_MESSAGE)
53
+ .describe(
54
+ `The exact thing to remember, in the user's words. Budgets by lane: instruction_policy at most ${AGENT_AUTHORED_INSTRUCTION_POLICY_CONTENT_MAX_CHARS} characters because it is composed into the prompt of every session it applies to, preference at most ${AGENT_AUTHORED_PREFERENCE_CONTENT_MAX_CHARS} (its short descriptor is what is composed; the content is retrieved on demand), knowledge at most ${REMEMBER_CONTENT_MAX_CHARS}.`,
55
+ ),
56
+ reason: z.string().trim().min(1).max(4_096),
57
+ };
58
+
59
+ /**
60
+ * Explicit user-directed remember. Content becomes exact task-note evidence,
61
+ * promotion runs through the frozen workspace learning policy, and activation
62
+ * is either automatic (preference under Automatic mode) or requires exactly one
63
+ * bound human confirmation through the built-in `request_human_input` tool.
64
+ * Tool input cannot select scope beyond the workspace, active authority, or
65
+ * another learning-policy source.
66
+ */
67
+ export function registerRememberTools(input: RegisterRememberToolsInput): void {
68
+ const router = input.router ?? createRememberRouter({ db: input.db });
69
+ input.server.registerTool(
70
+ "remember",
71
+ {
72
+ description:
73
+ "Durably remember something the user explicitly asked to keep for this workspace. Use lane=preference for how agents should act, lane=instruction_policy only when the user stated a hard always/never rule, lane=knowledge for a company/product/people fact. " +
74
+ `Write it short. A lane=instruction_policy rule is composed verbatim into the prompt of every session it applies to (every session in this workspace for a global rule, every session bound to the role for a role rule) for as long as it stays active, so keep it under ${AGENT_AUTHORED_INSTRUCTION_POLICY_CONTENT_MAX_CHARS} characters: one imperative rule in 1-3 sentences, no numbered steps, no examples, no rationale, no restating of defaults. At most three rules compose at once, so this is a standing budget you share. Keep a lane=preference under ${AGENT_AUTHORED_PREFERENCE_CONTENT_MAX_CHARS} characters; there only its short title and description are composed and the content is retrieved on demand, so length is retrieval cost rather than standing prompt cost. Prefer several small entries over one long one, and put procedure in a Document or Skill that the rule references instead of inlining it. ` +
75
+ "Under Automatic learning a preference activates immediately; otherwise the receipt returns status=confirmation_required with the exact `humanInput` payload: call `request_human_input` with it verbatim, then call `remember_confirm` with the returned requestId. Mandatory rules always need that confirmation. Do not use this for facts you merely inferred; use knowledge_propose or task notes for those. A confirmed lane=knowledge fact enters the human-reviewed Knowledge claim lifecycle; it does not become workspace memory, so do not expect to find it later through `memory_search`.",
76
+ inputSchema: {
77
+ lane: z.enum(["preference", "instruction_policy", "knowledge"]),
78
+ ...laneFields,
79
+ stableKey: z
80
+ .string()
81
+ .trim()
82
+ .min(1)
83
+ .max(128)
84
+ .regex(/^[a-z0-9](?:[a-z0-9._-]*[a-z0-9])?$/)
85
+ .optional(),
86
+ title: z.string().trim().min(1).max(PREFERENCE_REGISTRY_TITLE_MAX_CHARS).optional(),
87
+ description: z
88
+ .string()
89
+ .trim()
90
+ .min(1)
91
+ .max(PREFERENCE_REGISTRY_DESCRIPTOR_DESCRIPTION_MAX_CHARS)
92
+ .optional(),
93
+ target: WorkspaceInstructionPolicyTarget.optional(),
94
+ subject: z.string().trim().min(1).max(512).optional(),
95
+ },
96
+ },
97
+ async (request) => {
98
+ await input.authorize();
99
+ const { lane, operationId, content, reason } = request;
100
+ if (lane !== "knowledge" && content.length > laneContentMaxChars[lane]) {
101
+ return input.json({
102
+ status: "not_remembered",
103
+ code: "content_too_long",
104
+ message: agentAuthoredDurableTextTooLongMessage({
105
+ kind: lane,
106
+ actualChars: content.length,
107
+ }),
108
+ });
109
+ }
110
+ const base = { operationId, content, reason, scope: "workspace" as const };
111
+ const rememberRequest =
112
+ lane === "preference"
113
+ ? {
114
+ ...base,
115
+ lane,
116
+ stableKey: request.stableKey ?? `remember.${operationId.replaceAll("-", "")}`,
117
+ title: request.title ?? content.slice(0, 80),
118
+ description: request.description ?? content.slice(0, 200),
119
+ }
120
+ : lane === "instruction_policy"
121
+ ? { ...base, lane, target: request.target }
122
+ : { ...base, lane, subject: request.subject ?? content.slice(0, 80) };
123
+ return input.json(
124
+ await router.remember({ attempt: input.attempt, request: rememberRequest }),
125
+ );
126
+ },
127
+ );
128
+
129
+ input.server.registerTool(
130
+ "remember_confirm",
131
+ {
132
+ description:
133
+ "Complete a `remember` that returned status=confirmation_required after the human answered the bound `request_human_input` question. For preference/instruction_policy pass proposalId and learning.receiptId (as decisionReceiptId) from that receipt; for knowledge pass claimId. Always pass the requestId returned by request_human_input. Activation only succeeds when the exact initiating human answered Save on this turn; otherwise the proposal stays for review.",
134
+ inputSchema: {
135
+ operationId: z.string().uuid(),
136
+ proposalId: z.string().uuid().optional(),
137
+ decisionReceiptId: z.string().uuid().optional(),
138
+ claimId: z.string().uuid().optional(),
139
+ humanInputRequestId: z.string().uuid(),
140
+ },
141
+ },
142
+ async (request) => {
143
+ await input.authorize();
144
+ const confirmRequest =
145
+ request.proposalId && request.decisionReceiptId
146
+ ? {
147
+ target: "proposal" as const,
148
+ operationId: request.operationId,
149
+ proposalId: request.proposalId,
150
+ decisionReceiptId: request.decisionReceiptId,
151
+ humanInputRequestId: request.humanInputRequestId,
152
+ }
153
+ : request.claimId
154
+ ? {
155
+ target: "knowledge_claim" as const,
156
+ operationId: request.operationId,
157
+ claimId: request.claimId,
158
+ humanInputRequestId: request.humanInputRequestId,
159
+ }
160
+ : null;
161
+ if (!confirmRequest) {
162
+ return input.json({
163
+ status: "not_confirmed",
164
+ code: "invalid_target",
165
+ message:
166
+ "Pass proposalId + decisionReceiptId (preference/instruction_policy) or claimId (knowledge).",
167
+ });
168
+ }
169
+ try {
170
+ return input.json(
171
+ await router.confirm({ attempt: input.attempt, request: confirmRequest }),
172
+ );
173
+ } catch (error) {
174
+ if (error instanceof RememberError) {
175
+ return input.json({ status: "not_confirmed", code: error.code, message: error.message });
176
+ }
177
+ throw error;
178
+ }
179
+ },
180
+ );
181
+ }
@@ -0,0 +1,44 @@
1
+ import type { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
2
+
3
+ /**
4
+ * Serve one first-party MCP POST and bind the tool handlers' `extra.signal` to
5
+ * the HTTP client's connection.
6
+ *
7
+ * The API builds a fresh transport plus `McpServer` per POST, so an MCP
8
+ * `notifications/cancelled` from the worker would land on a different Protocol
9
+ * instance and never reach the handler. The only cancellation evidence this
10
+ * process can observe is the HTTP request's own abort (`Request.signal`, which
11
+ * Bun fires when the client disconnects). Closing the transport runs the SDK
12
+ * `Protocol._onclose`, which aborts every in-flight request handler's
13
+ * controller, so a blocking tool such as `session_wait` stops promptly when the
14
+ * worker drops the call (Steer, Pause, turn interruption).
15
+ *
16
+ * In JSON-response mode the SDK's `handleRequest` promise never settles once
17
+ * its stream mapping is cleared, so the race resolves a client-gone response
18
+ * instead of leaving the route handler pending.
19
+ */
20
+ export async function handleMcpRequestWithClientAbort(
21
+ transport: Pick<WebStandardStreamableHTTPServerTransport, "handleRequest" | "close">,
22
+ request: Request,
23
+ clientSignal: AbortSignal | undefined,
24
+ ): Promise<Response> {
25
+ if (!clientSignal) return await transport.handleRequest(request);
26
+ const clientGone = () => new Response(null, { status: 499 });
27
+ if (clientSignal.aborted) {
28
+ await transport.close().catch(() => undefined);
29
+ return clientGone();
30
+ }
31
+ let onAbort: (() => void) | null = null;
32
+ const aborted = new Promise<Response>((resolve) => {
33
+ onAbort = () => {
34
+ void transport.close().catch(() => undefined);
35
+ resolve(clientGone());
36
+ };
37
+ clientSignal.addEventListener("abort", onAbort, { once: true });
38
+ });
39
+ try {
40
+ return await Promise.race([transport.handleRequest(request), aborted]);
41
+ } finally {
42
+ if (onAbort) clientSignal.removeEventListener("abort", onAbort);
43
+ }
44
+ }
@@ -301,6 +301,7 @@ function toolIdentity(
301
301
  kind: tool.kind,
302
302
  id: id.value,
303
303
  ...(tool.optional !== undefined ? { optional: tool.optional } : {}),
304
+ ...(tool.eager !== undefined ? { eager: tool.eager } : {}),
304
305
  },
305
306
  truncatedFieldCount: Number(id.truncated),
306
307
  };