@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.
- package/dist/api-websocket.d.ts +18 -0
- package/dist/app.js +1 -1
- package/dist/auth/managed-auth.d.ts +6 -0
- package/dist/{chunk-WCXHI3FF.js → chunk-3TP54PPX.js} +26692 -15389
- package/dist/chunk-3TP54PPX.js.map +1 -0
- package/dist/codemode.d.ts +8 -1
- package/dist/company-brain-okf.d.ts +24 -0
- package/dist/connected-machine-computer-access.d.ts +11 -0
- package/dist/connection-authority-owner.d.ts +59 -0
- package/dist/connection-ownership.d.ts +47 -0
- package/dist/controller-data-plane.d.ts +16 -0
- package/dist/editable-artifact-websocket.d.ts +5 -15
- package/dist/editable-artifact-workspace-files.d.ts +1 -1
- package/dist/http/api-error.d.ts +10 -0
- package/dist/http/cors.d.ts +5 -0
- package/dist/http/interaction-control-error.d.ts +13 -0
- package/dist/http/sse.d.ts +2 -3
- package/dist/index.d.ts +5 -2
- package/dist/index.js +326 -82
- package/dist/index.js.map +1 -1
- package/dist/integrations/atlassian.d.ts +17 -0
- package/dist/integrations/fiken.d.ts +7 -2
- package/dist/integrations/google-drive.d.ts +18 -0
- package/dist/integrations/oauth-client.d.ts +22 -3
- package/dist/integrations/oauth-profiles.d.ts +195 -0
- package/dist/integrations/personal-github-repositories.d.ts +45 -0
- package/dist/integrations/personal-github.d.ts +40 -0
- package/dist/integrations/pr-review-provider.d.ts +22 -0
- package/dist/integrations/provider-oauth.d.ts +8 -0
- package/dist/integrations/slack-app-home.d.ts +24 -0
- package/dist/integrations/slack-bot.d.ts +157 -9
- package/dist/integrations/slack-interactions.d.ts +39 -5
- package/dist/integrations/slack-routing.d.ts +120 -0
- package/dist/integrations/social-oauth.d.ts +5 -0
- package/dist/interaction-frame-proxy.d.ts +68 -0
- package/dist/mcp/company-brain-governed-writes.d.ts +26 -0
- package/dist/mcp/company-profile-agent-admin.d.ts +44 -0
- package/dist/mcp/documents.d.ts +1 -0
- package/dist/mcp/remember.d.ts +28 -0
- package/dist/mcp/request-abort.d.ts +19 -0
- package/dist/mcp/scheduled-task-view.d.ts +4 -4
- package/dist/mcp/server.d.ts +15 -2
- package/dist/mcp/session-view.d.ts +4 -0
- package/dist/mcp/session-wait.d.ts +137 -0
- package/dist/routes/automations.d.ts +4 -0
- package/dist/routes/billing.d.ts +5 -0
- package/dist/routes/browser-sessions.d.ts +4 -1
- package/dist/routes/company-brain.d.ts +3 -0
- package/dist/routes/company-profile.d.ts +2 -1
- package/dist/routes/connection-authorities.d.ts +3 -0
- package/dist/routes/personal-github-git-broker.d.ts +29 -0
- package/dist/routes/personal-github.d.ts +3 -0
- package/dist/routes/pr-review.d.ts +3 -0
- package/dist/routes/sessions.d.ts +1 -0
- package/dist/routes/user-resource-authorities.d.ts +3 -0
- package/dist/routes/workspace-capture.d.ts +10 -2
- package/dist/routes/workspace-learning.d.ts +3 -0
- package/dist/routes/workspaces.d.ts +19 -0
- package/dist/sandbox/channel-a.d.ts +11 -1
- package/dist/sandbox/connection-authority.d.ts +3 -0
- package/dist/sandbox/enrollment.d.ts +2 -0
- package/dist/sandbox/machines.d.ts +2 -2
- package/dist/sandbox/metrics-ingestion.d.ts +10 -20
- package/dist/sandbox/viewer.d.ts +24 -1
- package/dist/sandbox-file-artifacts.d.ts +11 -0
- package/dist/scheduled-task-deletion.d.ts +14 -0
- package/dist/slack-reaction-files.d.ts +1 -1
- package/dist/temporal-schedule-cleanup.d.ts +1 -0
- package/package.json +20 -18
- package/src/api-websocket.ts +23 -0
- package/src/app.ts +161 -23
- package/src/auth/managed-auth.ts +25 -3
- package/src/codemode.ts +53 -23
- package/src/codex-realtime.ts +8 -2
- package/src/company-brain-okf.ts +340 -0
- package/src/connected-machine-computer-access.ts +33 -0
- package/src/connection-authority-owner.ts +61 -0
- package/src/connection-ownership.ts +180 -0
- package/src/controller-data-plane.ts +47 -0
- package/src/editable-artifact-native-kernel.ts +26 -15
- package/src/editable-artifact-office-import.ts +28 -3
- package/src/editable-artifact-production.ts +13 -7
- package/src/editable-artifact-websocket.ts +11 -18
- package/src/editable-artifact-workspace-files.ts +31 -6
- package/src/http/api-error.ts +28 -0
- package/src/http/auth.ts +4 -0
- package/src/http/cors.ts +35 -0
- package/src/http/interaction-control-error.ts +164 -0
- package/src/http/sse.ts +112 -66
- package/src/index.ts +46 -6
- package/src/integrations/atlassian.ts +146 -35
- package/src/integrations/fiken.ts +102 -22
- package/src/integrations/google-drive.ts +301 -92
- package/src/integrations/oauth-client.ts +272 -143
- package/src/integrations/oauth-profiles.ts +477 -0
- package/src/integrations/personal-github-repositories.ts +445 -0
- package/src/integrations/personal-github.ts +705 -0
- package/src/integrations/pr-review-provider.ts +246 -0
- package/src/integrations/provider-oauth.ts +121 -5
- package/src/integrations/slack-app-home.ts +300 -0
- package/src/integrations/slack-bot.ts +342 -57
- package/src/integrations/slack-interactions.ts +1898 -259
- package/src/integrations/slack-routing.ts +337 -0
- package/src/integrations/social-oauth.ts +25 -0
- package/src/interaction-frame-proxy.ts +409 -0
- package/src/mcp/company-brain-governed-writes.ts +262 -0
- package/src/mcp/company-profile-agent-admin.ts +205 -0
- package/src/mcp/documents.ts +37 -6
- package/src/mcp/files.ts +16 -1
- package/src/mcp/remember.ts +181 -0
- package/src/mcp/request-abort.ts +44 -0
- package/src/mcp/scheduled-task-view.ts +1 -0
- package/src/mcp/server.ts +719 -369
- package/src/mcp/session-view.ts +54 -0
- package/src/mcp/session-wait.ts +554 -0
- package/src/model-catalog.ts +20 -12
- package/src/routes/api-integrations.ts +4 -0
- package/src/routes/api-keys.ts +1 -0
- package/src/routes/automations.ts +534 -0
- package/src/routes/billing.ts +57 -1
- package/src/routes/browser-sessions.ts +329 -79
- package/src/routes/channels.ts +17 -1
- package/src/routes/codex.ts +50 -0
- package/src/routes/company-brain.ts +367 -0
- package/src/routes/company-profile.ts +1 -1
- package/src/routes/computer-sessions.ts +320 -93
- package/src/routes/connection-authorities.ts +139 -0
- package/src/routes/connections.ts +144 -34
- package/src/routes/documents.ts +353 -4
- package/src/routes/editable-artifacts.ts +11 -3
- package/src/routes/enrollments.ts +116 -26
- package/src/routes/environments.ts +127 -55
- package/src/routes/files.ts +72 -11
- package/src/routes/install.ts +82 -28
- package/src/routes/integration-facets.ts +29 -0
- package/src/routes/interaction-resources.ts +20 -2
- package/src/routes/machines.ts +275 -13
- package/src/routes/organization-memberships.ts +717 -28
- package/src/routes/packs.ts +4 -3
- package/src/routes/personal-github-git-broker.ts +785 -0
- package/src/routes/personal-github.ts +319 -0
- package/src/routes/pr-review.ts +531 -0
- package/src/routes/rigs.ts +104 -39
- package/src/routes/scheduled-tasks.ts +19 -21
- package/src/routes/sessions.ts +775 -38
- package/src/routes/social.ts +14 -2
- package/src/routes/supergrok.ts +23 -4
- package/src/routes/transcription-recordings.ts +8 -2
- package/src/routes/user-resource-authorities.ts +138 -0
- package/src/routes/workspace-artifacts.ts +4 -1
- package/src/routes/workspace-capture.ts +21 -0
- package/src/routes/workspace-learning.ts +228 -0
- package/src/routes/workspaces.ts +59 -6
- package/src/sandbox/auth-callout.ts +38 -12
- package/src/sandbox/channel-a.ts +242 -19
- package/src/sandbox/connection-authority.ts +3 -0
- package/src/sandbox/enrollment.ts +15 -3
- package/src/sandbox/machines.ts +186 -64
- package/src/sandbox/metrics-ingestion.ts +220 -58
- package/src/sandbox/viewer.ts +243 -19
- package/src/sandbox-file-artifacts.ts +329 -0
- package/src/scheduled-task-deletion.ts +127 -0
- package/src/slack-reaction-files.ts +16 -5
- package/src/temporal-schedule-cleanup.ts +13 -0
- 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
|
+
}
|
package/src/mcp/documents.ts
CHANGED
|
@@ -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
|
|
75
|
-
|
|
76
|
-
|
|
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(
|
|
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(
|
|
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
|
};
|