@opengeni/api-router 2.1.0-canary.0 → 2.3.2-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/app.js +1 -1
- package/dist/auth/managed-auth.d.ts +5 -2
- package/dist/auth/managed-email.d.ts +29 -0
- package/dist/auth/organization-user-setup.d.ts +57 -0
- package/dist/{chunk-QKDFBBUE.js → chunk-IBV7Z6F4.js} +3872 -1845
- package/dist/chunk-IBV7Z6F4.js.map +1 -0
- package/dist/index.js +1 -1
- package/dist/integrations/slack-app-home.d.ts +1 -1
- package/dist/integrations/slack-bot.d.ts +8 -0
- package/dist/integrations/slack-interactions.d.ts +35 -2
- package/dist/mcp/server.d.ts +1 -1
- package/dist/mcp/session-view.d.ts +1 -0
- package/dist/routes/automations.d.ts +13 -0
- package/dist/routes/insights.d.ts +2 -1
- package/dist/routes/managed-onboarding.d.ts +29 -0
- package/dist/routes/pr-review-github.d.ts +3 -0
- package/package.json +18 -18
- package/src/app.ts +64 -5
- package/src/auth/managed-auth.ts +29 -34
- package/src/auth/managed-email.ts +174 -0
- package/src/auth/organization-user-setup.ts +217 -0
- package/src/http/auth.ts +15 -0
- package/src/http/sse.ts +62 -13
- package/src/integrations/slack-app-home.ts +2 -2
- package/src/integrations/slack-bot.ts +5 -0
- package/src/integrations/slack-interactions.ts +653 -71
- package/src/integrations/slack-routing.ts +25 -12
- package/src/mcp/company-brain-governed-writes.ts +4 -4
- package/src/mcp/company-profile-agent-admin.ts +11 -18
- package/src/mcp/remember.ts +4 -4
- package/src/mcp/server.ts +50 -4
- package/src/mcp/session-view.ts +8 -2
- package/src/routes/automations.ts +3 -3
- package/src/routes/documents.ts +136 -3
- package/src/routes/insights.ts +61 -19
- package/src/routes/managed-onboarding.ts +317 -0
- package/src/routes/organization-memberships.ts +212 -155
- package/src/routes/pr-review-github.ts +844 -0
- package/src/routes/pr-review.ts +20 -0
- package/src/routes/rigs.ts +37 -4
- package/src/routes/sessions.ts +101 -0
- package/src/sandbox/channel-a.ts +34 -7
- package/src/sandbox/viewer.ts +47 -11
- package/dist/chunk-QKDFBBUE.js.map +0 -1
|
@@ -175,11 +175,24 @@ function labelFor(
|
|
|
175
175
|
* override beats configuration beats derivation beats asking.
|
|
176
176
|
*/
|
|
177
177
|
export function resolveSlackWorkspaceRoute(input: SlackRouteInputs): SlackRouteResolution {
|
|
178
|
+
// A personal workspace is only ever a destination for that person's own bot
|
|
179
|
+
// DM. Offering it in a channel would be wrong twice over: routing a shared
|
|
180
|
+
// conversation into one member's private space hides it from everyone else
|
|
181
|
+
// in the channel, and - because managed tenancy provisions a personal
|
|
182
|
+
// workspace for every member - counting it as a candidate means nobody ever
|
|
183
|
+
// has exactly one. That defeats the sole-candidate rule below, so an
|
|
184
|
+
// organization with a single shared workspace would be asked to choose in
|
|
185
|
+
// every channel despite having no choice to make.
|
|
186
|
+
const directMessage = isSlackDirectMessageConversation(input.entry);
|
|
187
|
+
const candidates = directMessage
|
|
188
|
+
? input.candidates
|
|
189
|
+
: input.candidates.filter((candidate) => !candidate.personal);
|
|
190
|
+
|
|
178
191
|
const installation = {
|
|
179
192
|
kind: "resolved" as const,
|
|
180
193
|
accountId: input.home.accountId,
|
|
181
194
|
workspaceId: input.home.workspaceId,
|
|
182
|
-
label: labelFor(
|
|
195
|
+
label: labelFor(candidates, input.home.workspaceId),
|
|
183
196
|
source: "installation" as const,
|
|
184
197
|
};
|
|
185
198
|
|
|
@@ -206,7 +219,7 @@ export function resolveSlackWorkspaceRoute(input: SlackRouteInputs): SlackRouteR
|
|
|
206
219
|
kind: "resolved",
|
|
207
220
|
accountId: input.threadTenancy.accountId,
|
|
208
221
|
workspaceId: input.threadTenancy.workspaceId,
|
|
209
|
-
label: labelFor(
|
|
222
|
+
label: labelFor(candidates, input.threadTenancy.workspaceId),
|
|
210
223
|
source: "thread",
|
|
211
224
|
};
|
|
212
225
|
}
|
|
@@ -221,13 +234,13 @@ export function resolveSlackWorkspaceRoute(input: SlackRouteInputs): SlackRouteR
|
|
|
221
234
|
? parseSlackWorkspacePrefix(splitSlackLeadingMention(input.entry.text, input.botUserId).rest)
|
|
222
235
|
: null;
|
|
223
236
|
if (prefix) {
|
|
224
|
-
const named = matchCandidate(
|
|
237
|
+
const named = matchCandidate(candidates, prefix.requested);
|
|
225
238
|
if (named === "ambiguous" || !named) {
|
|
226
239
|
return {
|
|
227
240
|
kind: "denied",
|
|
228
241
|
reason: "no_access_to_named",
|
|
229
242
|
requested: prefix.requested,
|
|
230
|
-
candidates:
|
|
243
|
+
candidates: candidates,
|
|
231
244
|
};
|
|
232
245
|
}
|
|
233
246
|
return {
|
|
@@ -245,7 +258,7 @@ export function resolveSlackWorkspaceRoute(input: SlackRouteInputs): SlackRouteR
|
|
|
245
258
|
kind: "resolved",
|
|
246
259
|
accountId: input.channelRoute.targetAccountId,
|
|
247
260
|
workspaceId: input.channelRoute.targetWorkspaceId,
|
|
248
|
-
label: labelFor(
|
|
261
|
+
label: labelFor(candidates, input.channelRoute.targetWorkspaceId),
|
|
249
262
|
source: "channel",
|
|
250
263
|
};
|
|
251
264
|
}
|
|
@@ -254,13 +267,13 @@ export function resolveSlackWorkspaceRoute(input: SlackRouteInputs): SlackRouteR
|
|
|
254
267
|
// lands in their own workspace unless they chose otherwise. The personal
|
|
255
268
|
// workspace id is DERIVED from an active organization membership pointer;
|
|
256
269
|
// it is never accepted from a Slack payload or a route row.
|
|
257
|
-
if (
|
|
270
|
+
if (directMessage) {
|
|
258
271
|
if (input.dmRoute) {
|
|
259
272
|
return {
|
|
260
273
|
kind: "resolved",
|
|
261
274
|
accountId: input.dmRoute.targetAccountId,
|
|
262
275
|
workspaceId: input.dmRoute.targetWorkspaceId,
|
|
263
|
-
label: labelFor(
|
|
276
|
+
label: labelFor(candidates, input.dmRoute.targetWorkspaceId),
|
|
264
277
|
source: "dm_route",
|
|
265
278
|
};
|
|
266
279
|
}
|
|
@@ -269,7 +282,7 @@ export function resolveSlackWorkspaceRoute(input: SlackRouteInputs): SlackRouteR
|
|
|
269
282
|
kind: "resolved",
|
|
270
283
|
accountId: input.home.accountId,
|
|
271
284
|
workspaceId: input.personalWorkspaceId,
|
|
272
|
-
label: labelFor(
|
|
285
|
+
label: labelFor(candidates, input.personalWorkspaceId),
|
|
273
286
|
source: "dm_personal",
|
|
274
287
|
};
|
|
275
288
|
}
|
|
@@ -280,8 +293,8 @@ export function resolveSlackWorkspaceRoute(input: SlackRouteInputs): SlackRouteR
|
|
|
280
293
|
|
|
281
294
|
// 5. One workspace is not a choice. This is what keeps the flag quiet for
|
|
282
295
|
// installs that only ever had one workspace.
|
|
283
|
-
const sole =
|
|
284
|
-
if (
|
|
296
|
+
const sole = candidates[0];
|
|
297
|
+
if (candidates.length === 1 && sole) {
|
|
285
298
|
return {
|
|
286
299
|
kind: "resolved",
|
|
287
300
|
accountId: sole.accountId,
|
|
@@ -291,13 +304,13 @@ export function resolveSlackWorkspaceRoute(input: SlackRouteInputs): SlackRouteR
|
|
|
291
304
|
};
|
|
292
305
|
}
|
|
293
306
|
|
|
294
|
-
if (
|
|
307
|
+
if (candidates.length === 0) {
|
|
295
308
|
return { kind: "denied", reason: "no_candidates", requested: null, candidates: [] };
|
|
296
309
|
}
|
|
297
310
|
|
|
298
311
|
// 6. Genuinely ambiguous. Until the picker exists, keep the installation's
|
|
299
312
|
// workspace rather than inventing an answer.
|
|
300
|
-
return input.askEnabled ? { kind: "ask", candidates:
|
|
313
|
+
return input.askEnabled ? { kind: "ask", candidates: candidates } : installation;
|
|
301
314
|
}
|
|
302
315
|
|
|
303
316
|
/**
|
|
@@ -125,7 +125,7 @@ export function registerCompanyBrainGovernedWriteTools(
|
|
|
125
125
|
{
|
|
126
126
|
description:
|
|
127
127
|
"Atomically promote one still-active note from this exact root task tree into an inactive workspace instruction-policy draft. The note bytes remain exact evidence and draft content. " +
|
|
128
|
-
`Once a human activates the draft, those bytes are composed verbatim into the prompt of every session the target applies to, so a note over ${AGENT_AUTHORED_INSTRUCTION_POLICY_CONTENT_MAX_CHARS} characters is rejected here rather than truncated: write a fresh
|
|
128
|
+
`Use this only for a universal always-on rule, never for an incident, fact, decision, outcome, or conditional procedure. Once a human activates the draft, those bytes are composed verbatim into the prompt of every session the target applies to, so a note over ${AGENT_AUTHORED_INSTRUCTION_POLICY_CONTENT_MAX_CHARS} characters is rejected here rather than truncated: write a fresh minimal imperative note instead of promoting a long working note. ` +
|
|
129
129
|
"The frozen learning policy records a decision receipt, but mandatory policy still requires human activation even under Automatic; this never widens scope.",
|
|
130
130
|
inputSchema: {
|
|
131
131
|
...taskNotePromotion,
|
|
@@ -149,7 +149,7 @@ export function registerCompanyBrainGovernedWriteTools(
|
|
|
149
149
|
"task_note_promote_preference",
|
|
150
150
|
{
|
|
151
151
|
description:
|
|
152
|
-
"Atomically promote one still-active note from this exact root task tree into a workspace preference
|
|
152
|
+
"Atomically promote one still-active note from this exact root task tree into a workspace Skill proposal backed by the structured preference authority. Use this for reusable conditional how-to guidance, never for an incident, fact, decision, outcome, or universal always-on rule. The note bytes remain exact evidence and full proposal content. " +
|
|
153
153
|
`The title and description you supply are what gets composed into every session prompt, so write them as one short imperative statement; the note content is retrieved on demand and a note over ${AGENT_AUTHORED_PREFERENCE_CONTENT_MAX_CHARS} characters is rejected here rather than truncated. ` +
|
|
154
154
|
"Under Suggest the proposal waits for human review; under Automatic an eligible decision is activated through the preference lifecycle and remains undoable. This never widens scope.",
|
|
155
155
|
inputSchema: {
|
|
@@ -186,7 +186,7 @@ export function registerCompanyBrainGovernedWriteTools(
|
|
|
186
186
|
{
|
|
187
187
|
description:
|
|
188
188
|
"Materialize an evidence-backed inactive workspace instruction-policy draft. " +
|
|
189
|
-
`Once a human activates it, this content is composed verbatim into the prompt of every session the target applies to (every session in this workspace for a global charter or policy, every session bound to the role for a role policy), so keep it under ${AGENT_AUTHORED_INSTRUCTION_POLICY_CONTENT_MAX_CHARS} characters. ${AGENT_AUTHORED_DURABLE_TEXT_STYLE} ` +
|
|
189
|
+
`Use this only for a minimal universal rule, never for an incident, fact, decision, outcome, or conditional procedure. Once a human activates it, this content is composed verbatim into the prompt of every session the target applies to (every session in this workspace for a global charter or policy, every session bound to the role for a role policy), so keep it under ${AGENT_AUTHORED_INSTRUCTION_POLICY_CONTENT_MAX_CHARS} characters. ${AGENT_AUTHORED_DURABLE_TEXT_STYLE} ` +
|
|
190
190
|
"The frozen learning policy records a decision receipt, but this tool cannot activate mandatory behavior, including when learning mode is Automatic; a human must activate the draft.",
|
|
191
191
|
inputSchema: {
|
|
192
192
|
...evidence,
|
|
@@ -219,7 +219,7 @@ export function registerCompanyBrainGovernedWriteTools(
|
|
|
219
219
|
"preference_propose",
|
|
220
220
|
{
|
|
221
221
|
description:
|
|
222
|
-
"Materialize an evidence-backed workspace preference
|
|
222
|
+
"Materialize an evidence-backed workspace Skill proposal in the structured preference authority. Use this only for reusable conditional how-to guidance, never for an incident, fact, decision, outcome, or universal always-on rule. " +
|
|
223
223
|
`Its short title and description are what get composed into every session prompt; the content is retrieved on demand, so its length is retrieval cost rather than standing prompt cost. Keep the content under ${AGENT_AUTHORED_PREFERENCE_CONTENT_MAX_CHARS} characters. ${AGENT_AUTHORED_DURABLE_TEXT_STYLE} ` +
|
|
224
224
|
"Under Suggest it stays inactive for human review; under Automatic an eligible decision is activated through the governed preference lifecycle with an undoable receipt. It never creates mandatory authority.",
|
|
225
225
|
inputSchema: {
|
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
import {
|
|
2
|
-
AGENT_AUTHORED_COMPANY_PROFILE_CONTENT_MAX_UTF8_BYTES,
|
|
3
2
|
AGENT_AUTHORED_COMPANY_PROFILE_ENTRY_MAX_CHARS,
|
|
4
3
|
AGENT_AUTHORED_COMPANY_PROFILE_SCALAR_MAX_CHARS,
|
|
5
4
|
AGENT_AUTHORED_COMPANY_PROFILE_TOO_LONG_MESSAGE,
|
|
6
5
|
AgentAuthoredCompanyProfileContent,
|
|
7
|
-
COMPANY_PROFILE_ENTRY_MAX_COUNT,
|
|
8
6
|
COMPANY_PROFILE_REASON_MAX_CHARS,
|
|
9
7
|
COMPANY_PROFILE_STABLE_KEY_MAX_CHARS,
|
|
10
8
|
normalizeCompanyProfileStableKey,
|
|
@@ -32,9 +30,9 @@ export type RegisterCompanyProfileAgentAdminToolsInput = {
|
|
|
32
30
|
router?: Pick<ReturnType<typeof createCompanyProfileAgentAdminRouter>, "propose" | "confirm">;
|
|
33
31
|
};
|
|
34
32
|
|
|
35
|
-
// Agent-only bounds. The human `account:admin`
|
|
36
|
-
// `COMPANY_PROFILE_*` limits;
|
|
37
|
-
//
|
|
33
|
+
// Agent-only bounds. The human `account:admin` API keeps the wider historical
|
|
34
|
+
// `COMPANY_PROFILE_*` limits; the current agent tool authors only the two
|
|
35
|
+
// always-on identity fields and therefore gets a much smaller budget.
|
|
38
36
|
const scalar = z
|
|
39
37
|
.string()
|
|
40
38
|
.trim()
|
|
@@ -55,8 +53,7 @@ const entry = z.object({
|
|
|
55
53
|
AGENT_AUTHORED_COMPANY_PROFILE_TOO_LONG_MESSAGE,
|
|
56
54
|
),
|
|
57
55
|
});
|
|
58
|
-
const
|
|
59
|
-
const DEFAULT_PROPOSAL_REASON = "Activate agent-proposed organization company profile";
|
|
56
|
+
const DEFAULT_PROPOSAL_REASON = "Activate agent-proposed organization identity";
|
|
60
57
|
const STABLE_KEY_WORDS = 6;
|
|
61
58
|
|
|
62
59
|
type EntryInput = z.infer<typeof entry>;
|
|
@@ -127,18 +124,14 @@ export function registerCompanyProfileAgentAdminTools(
|
|
|
127
124
|
"company_profile_propose",
|
|
128
125
|
{
|
|
129
126
|
description:
|
|
130
|
-
"Prepare
|
|
131
|
-
"Once activated,
|
|
132
|
-
`
|
|
127
|
+
"Prepare the organization's small, stable identity: identity says who the organization is, and mission says why it exists. " +
|
|
128
|
+
"Once activated, both fields are mandatory prompt context in every root session for the whole organization, so use one plain descriptive statement per field with no products, customers, goals, constraints, procedures, or marketing copy. Those details belong in organization-scoped Documents and are retrieved only when relevant. " +
|
|
129
|
+
`Each field is bounded to ${AGENT_AUTHORED_COMPANY_PROFILE_SCALAR_MAX_CHARS} characters for agent-authored proposals. ` +
|
|
133
130
|
"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
131
|
inputSchema: {
|
|
135
132
|
operationId: z.string().uuid(),
|
|
136
133
|
identity: scalar,
|
|
137
134
|
mission: scalar,
|
|
138
|
-
products: entries,
|
|
139
|
-
customers: entries,
|
|
140
|
-
goals: entries,
|
|
141
|
-
constraints: entries,
|
|
142
135
|
reason: z.string().trim().min(1).max(COMPANY_PROFILE_REASON_MAX_CHARS).optional(),
|
|
143
136
|
},
|
|
144
137
|
},
|
|
@@ -147,10 +140,10 @@ export function registerCompanyProfileAgentAdminTools(
|
|
|
147
140
|
const parsed = AgentAuthoredCompanyProfileContent.safeParse({
|
|
148
141
|
identity: request.identity,
|
|
149
142
|
mission: request.mission,
|
|
150
|
-
products:
|
|
151
|
-
customers:
|
|
152
|
-
goals:
|
|
153
|
-
constraints:
|
|
143
|
+
products: [],
|
|
144
|
+
customers: [],
|
|
145
|
+
goals: [],
|
|
146
|
+
constraints: [],
|
|
154
147
|
});
|
|
155
148
|
if (!parsed.success) {
|
|
156
149
|
return input.json({
|
package/src/mcp/remember.ts
CHANGED
|
@@ -70,9 +70,9 @@ export function registerRememberTools(input: RegisterRememberToolsInput): void {
|
|
|
70
70
|
"remember",
|
|
71
71
|
{
|
|
72
72
|
description:
|
|
73
|
-
"Durably remember something the user explicitly asked to keep for this workspace.
|
|
74
|
-
`Write
|
|
75
|
-
"Under Automatic learning a
|
|
73
|
+
"Durably remember something the user explicitly asked to keep for this workspace. Route by purpose: lane=knowledge for a fact, decision, incident, bug fix, or outcome that should become searchable Memory; lane=preference creates a Skill for reusable conditional how-to guidance; lane=instruction_policy is only for a universal always/never rule that should apply to nearly every task. " +
|
|
74
|
+
`Write the instruction lane as the shortest complete rule, at most ${AGENT_AUTHORED_INSTRUCTION_POLICY_CONTENT_MAX_CHARS} characters and normally 1-3 imperative sentences, with no numbered steps, examples, rationale, or restated defaults. Keep a Skill under ${AGENT_AUTHORED_PREFERENCE_CONTENT_MAX_CHARS} characters; only its one-sentence descriptor is composed and the full instructions are retrieved on demand. Do not copy one item into multiple lanes. ` +
|
|
75
|
+
"Under Automatic learning a Skill may activate 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. Workspace instructions and Memory always need that confirmation. Do not use this for facts you merely inferred; use knowledge_propose or task notes for those. Confirmed lane=knowledge content keeps its reviewed claim provenance and materializes its exact approved text into Memory for later `memory_search` retrieval.",
|
|
76
76
|
inputSchema: {
|
|
77
77
|
lane: z.enum(["preference", "instruction_policy", "knowledge"]),
|
|
78
78
|
...laneFields,
|
|
@@ -130,7 +130,7 @@ export function registerRememberTools(input: RegisterRememberToolsInput): void {
|
|
|
130
130
|
"remember_confirm",
|
|
131
131
|
{
|
|
132
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;
|
|
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; confirmed knowledge is materialized into searchable Memory from the exact approved text.",
|
|
134
134
|
inputSchema: {
|
|
135
135
|
operationId: z.string().uuid(),
|
|
136
136
|
proposalId: z.string().uuid().optional(),
|
package/src/mcp/server.ts
CHANGED
|
@@ -38,6 +38,7 @@ import {
|
|
|
38
38
|
SESSION_GOAL_SUCCESS_CRITERIA_MAX_BYTES,
|
|
39
39
|
SESSION_GOAL_TEXT_MAX_BYTES,
|
|
40
40
|
SESSION_INSTRUCTIONS_MAX_CHARACTERS,
|
|
41
|
+
MAX_SELECTED_VARIABLE_SETS,
|
|
41
42
|
sessionGoalUtf8Bytes,
|
|
42
43
|
TASK_NOTE_LIST_DEFAULT_LIMIT,
|
|
43
44
|
TASK_NOTE_LIST_MAX_LIMIT,
|
|
@@ -755,7 +756,7 @@ export function buildOpenGeniMcpServer(
|
|
|
755
756
|
"set_session_title",
|
|
756
757
|
{
|
|
757
758
|
description:
|
|
758
|
-
"Set this session's display title to a concise 3-7 word
|
|
759
|
+
"Set this session's display title to a concise 3-7 word topic label. Use a stable noun phrase about the actual task or subject, never a quote/prefix of a prompt, greeting, request boilerplate, URL, identifier, credential, token, or other sensitive value. Call once on a new session, then only when the topic materially changes. Never call it as routine setup after a continuation, resume, or interruption, or merely to reassert the same title. A human-set title cannot be replaced.",
|
|
759
760
|
inputSchema: { title: z4.string().min(1).max(200) },
|
|
760
761
|
},
|
|
761
762
|
async ({ title }) => {
|
|
@@ -3256,7 +3257,7 @@ function registerPreferenceRegistryTools(
|
|
|
3256
3257
|
"preference_registry_summary",
|
|
3257
3258
|
{
|
|
3258
3259
|
description:
|
|
3259
|
-
"List bounded deterministic descriptors for organization, workspace, and immutable initiating
|
|
3260
|
+
"List bounded deterministic Skill descriptors for organization, workspace, and the immutable initiating human, frozen to this exact attempt. Full Skill instructions are omitted; retrieve only a relevant returned handle.",
|
|
3260
3261
|
inputSchema: {},
|
|
3261
3262
|
},
|
|
3262
3263
|
async () => json(await getOrCreatePreferenceRegistrySnapshot(deps.db, attemptClaims())),
|
|
@@ -3266,7 +3267,7 @@ function registerPreferenceRegistryTools(
|
|
|
3266
3267
|
"preference_registry_get",
|
|
3267
3268
|
{
|
|
3268
3269
|
description:
|
|
3269
|
-
"Retrieve full
|
|
3270
|
+
"Retrieve the full instructions for one Skill in this exact attempt snapshot. Handles from another account, workspace, human, or attempt are rejected.",
|
|
3270
3271
|
inputSchema: { retrievalHandle: z4.string().min(1).max(512) },
|
|
3271
3272
|
},
|
|
3272
3273
|
async ({ retrievalHandle }) =>
|
|
@@ -4295,6 +4296,7 @@ function registerWorkspaceOrchestrationTools(
|
|
|
4295
4296
|
tools: z4.array(z4.unknown()).optional(),
|
|
4296
4297
|
mcpServers: z4.array(z4.unknown()).optional(),
|
|
4297
4298
|
variableSetId: z4.string().uuid().optional(),
|
|
4299
|
+
variableSetIds: z4.array(z4.string().uuid()).max(MAX_SELECTED_VARIABLE_SETS).optional(),
|
|
4298
4300
|
environmentId: z4.string().uuid().optional(),
|
|
4299
4301
|
rigId: z4.string().uuid().optional(),
|
|
4300
4302
|
model: z4
|
|
@@ -4345,6 +4347,26 @@ function registerWorkspaceOrchestrationTools(
|
|
|
4345
4347
|
.union([z4.literal("new"), z4.object({ groupId: z4.string().uuid() })])
|
|
4346
4348
|
.optional(),
|
|
4347
4349
|
})
|
|
4350
|
+
.superRefine((value, context) => {
|
|
4351
|
+
if (!value.variableSetIds) return;
|
|
4352
|
+
if (new Set(value.variableSetIds).size !== value.variableSetIds.length) {
|
|
4353
|
+
context.addIssue({
|
|
4354
|
+
code: z4.ZodIssueCode.custom,
|
|
4355
|
+
path: ["variableSetIds"],
|
|
4356
|
+
message: "variableSetIds must not contain duplicates",
|
|
4357
|
+
});
|
|
4358
|
+
}
|
|
4359
|
+
const singular = value.variableSetId ?? value.environmentId;
|
|
4360
|
+
if (singular === undefined) return;
|
|
4361
|
+
const expected = value.variableSetIds[value.variableSetIds.length - 1];
|
|
4362
|
+
if (singular !== expected) {
|
|
4363
|
+
context.addIssue({
|
|
4364
|
+
code: z4.ZodIssueCode.custom,
|
|
4365
|
+
path: ["variableSetId"],
|
|
4366
|
+
message: "variableSetId must match the last variableSetIds entry",
|
|
4367
|
+
});
|
|
4368
|
+
}
|
|
4369
|
+
})
|
|
4348
4370
|
.strict();
|
|
4349
4371
|
server.registerTool(
|
|
4350
4372
|
"session_create",
|
|
@@ -4355,6 +4377,11 @@ function registerWorkspaceOrchestrationTools(
|
|
|
4355
4377
|
},
|
|
4356
4378
|
async (args) => {
|
|
4357
4379
|
try {
|
|
4380
|
+
requireVariableSetsUseForMcpAttachments(grant, {
|
|
4381
|
+
variableSetIds: args.variableSetIds,
|
|
4382
|
+
variableSetId: args.variableSetId,
|
|
4383
|
+
environmentId: args.environmentId,
|
|
4384
|
+
});
|
|
4358
4385
|
if (callerSessionId !== null) {
|
|
4359
4386
|
await authorizeFirstPartySession(deps, grant, callerSessionId, "session.child.create");
|
|
4360
4387
|
}
|
|
@@ -4744,7 +4771,7 @@ function registerWorkspaceOrchestrationTools(
|
|
|
4744
4771
|
"set_other_session_title",
|
|
4745
4772
|
{
|
|
4746
4773
|
description:
|
|
4747
|
-
"Set another session's display title to a concise 3-7 word
|
|
4774
|
+
"Set another session's display title to a concise 3-7 word topic label. Use a stable noun phrase about the actual task or subject, never a quote/prefix of a prompt, greeting, request boilerplate, URL, identifier, credential, token, or other sensitive value. The target session must belong to this workspace. Replaces an existing automatic title unless a human has manually set it.",
|
|
4748
4775
|
inputSchema: {
|
|
4749
4776
|
session_id: z4.string().uuid(),
|
|
4750
4777
|
title: z4.string().min(1).max(200),
|
|
@@ -5342,6 +5369,25 @@ function requireVariableSetsUseForMcpAttachment(
|
|
|
5342
5369
|
}
|
|
5343
5370
|
}
|
|
5344
5371
|
|
|
5372
|
+
function requireVariableSetsUseForMcpAttachments(
|
|
5373
|
+
grant: AccessGrant,
|
|
5374
|
+
selection: {
|
|
5375
|
+
variableSetIds?: string[] | undefined;
|
|
5376
|
+
variableSetId?: string | undefined;
|
|
5377
|
+
environmentId?: string | undefined;
|
|
5378
|
+
},
|
|
5379
|
+
): void {
|
|
5380
|
+
const singular = selection.variableSetId ?? selection.environmentId;
|
|
5381
|
+
const variableSetIds = selection.variableSetIds ?? (singular ? [singular] : undefined);
|
|
5382
|
+
if (variableSetIds === undefined) return;
|
|
5383
|
+
if (!hasPermission(grant.permissions, "variable-sets:attach")) {
|
|
5384
|
+
throw new HTTPException(403, { message: "missing permission: variable-sets:attach" });
|
|
5385
|
+
}
|
|
5386
|
+
if (variableSetIds.length > 0 && !hasPermission(grant.permissions, "variable-sets:use")) {
|
|
5387
|
+
throw new HTTPException(403, { message: "missing permission: variable-sets:use" });
|
|
5388
|
+
}
|
|
5389
|
+
}
|
|
5390
|
+
|
|
5345
5391
|
/**
|
|
5346
5392
|
* Project one allowlisted GitHub App repository into the resource an agent or
|
|
5347
5393
|
* scheduled task attaches. Every listed repository is in the workspace
|
package/src/mcp/session-view.ts
CHANGED
|
@@ -534,6 +534,11 @@ export function boundSessionDetailMcp(
|
|
|
534
534
|
effectiveControl: unknown = session.effectiveControl,
|
|
535
535
|
maxBytes = SESSION_DETAIL_MCP_MAX_BYTES,
|
|
536
536
|
) {
|
|
537
|
+
const variableSetIds = session.variableSetIds;
|
|
538
|
+
// The plural selection is authoritative. Keep the singular/environment
|
|
539
|
+
// fields only as final-entry compatibility aliases so they cannot be read as
|
|
540
|
+
// complete attachment membership by newer consumers.
|
|
541
|
+
const variableSetId = variableSetIds.at(-1) ?? null;
|
|
537
542
|
const title = session.title === null ? null : modelStringProjection(session.title, 512);
|
|
538
543
|
const initialMessage = modelStringProjection(session.initialMessage, 4_000);
|
|
539
544
|
const instructions =
|
|
@@ -592,8 +597,9 @@ export function boundSessionDetailMcp(
|
|
|
592
597
|
sandboxGroupId: session.sandboxGroupId,
|
|
593
598
|
activeSandboxId: session.activeSandboxId,
|
|
594
599
|
activeEpoch: session.activeEpoch,
|
|
595
|
-
|
|
596
|
-
|
|
600
|
+
variableSetIds,
|
|
601
|
+
variableSetId,
|
|
602
|
+
environmentId: variableSetId,
|
|
597
603
|
rigId: session.rigId,
|
|
598
604
|
rigVersionId: session.rigVersionId,
|
|
599
605
|
firstPartyMcpPermissions: permissions.value,
|
|
@@ -278,7 +278,7 @@ export function registerAutomationRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
278
278
|
});
|
|
279
279
|
const bytes = new TextEncoder().encode(stableJson(request));
|
|
280
280
|
return c.json(
|
|
281
|
-
await
|
|
281
|
+
await acceptAutomationEvent(deps, source, {
|
|
282
282
|
deliveryKey:
|
|
283
283
|
request.deliveryId ?? `manual:${automationRequestDigest(source.adapterId, bytes)}`,
|
|
284
284
|
requestDigest: automationRequestDigest(source.adapterId, bytes),
|
|
@@ -335,7 +335,7 @@ export function registerAutomationRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
335
335
|
}
|
|
336
336
|
const requestDigest = automationRequestDigest(source.adapterId, rawBody);
|
|
337
337
|
try {
|
|
338
|
-
const result = await
|
|
338
|
+
const result = await acceptAutomationEvent(deps, source, {
|
|
339
339
|
deliveryKey: adapter.deliveryKey({
|
|
340
340
|
headers: c.req.raw.headers,
|
|
341
341
|
requestDigest,
|
|
@@ -357,7 +357,7 @@ export function registerAutomationRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
357
357
|
});
|
|
358
358
|
}
|
|
359
359
|
|
|
360
|
-
async function
|
|
360
|
+
export async function acceptAutomationEvent(
|
|
361
361
|
deps: ApiRouteDeps,
|
|
362
362
|
source: AutomationSourceSecret,
|
|
363
363
|
input: {
|
package/src/routes/documents.ts
CHANGED
|
@@ -7,6 +7,8 @@ import {
|
|
|
7
7
|
DocumentAuthorityReclassification,
|
|
8
8
|
DocumentBase,
|
|
9
9
|
DocumentDefaultCollectionBackfill,
|
|
10
|
+
DocumentDefaultCollectionBackfillAudit,
|
|
11
|
+
GetDocumentDefaultCollectionBackfillAuditQuery,
|
|
10
12
|
DocumentSearchRequest,
|
|
11
13
|
DocumentSearchResponse,
|
|
12
14
|
FileAsset,
|
|
@@ -15,6 +17,9 @@ import {
|
|
|
15
17
|
KnowledgeMemorySearchRequest,
|
|
16
18
|
ListDocumentAuthorityReclassificationsQuery,
|
|
17
19
|
ListDocumentAuthorityReclassificationsResponse,
|
|
20
|
+
ListDocumentDefaultCollectionBackfillRunsResponse,
|
|
21
|
+
ListDocumentMigrationAuditQuery,
|
|
22
|
+
ListOrganizationDocumentAuthorityReclassificationsResponse,
|
|
18
23
|
MoveDocumentRequest,
|
|
19
24
|
ReclassifyDocumentAuthorityRequest,
|
|
20
25
|
RunDocumentDefaultCollectionBackfillRequest,
|
|
@@ -40,10 +45,13 @@ import {
|
|
|
40
45
|
getDocument,
|
|
41
46
|
getDocumentOriginalFile,
|
|
42
47
|
getDocumentBase,
|
|
48
|
+
getDocumentDefaultCollectionBackfillAudit,
|
|
43
49
|
listAccessibleDocuments,
|
|
44
50
|
listDocumentAuthorityReclassifications,
|
|
51
|
+
listDocumentDefaultCollectionBackfillRuns,
|
|
45
52
|
listDocumentBasesEnsuringDefault,
|
|
46
53
|
listDocuments,
|
|
54
|
+
listOrganizationDocumentAuthorityReclassifications,
|
|
47
55
|
moveDocumentToBase,
|
|
48
56
|
queueDocumentForReindex,
|
|
49
57
|
reclassifyDocumentAuthority,
|
|
@@ -132,6 +140,106 @@ export function registerDocumentRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
132
140
|
}
|
|
133
141
|
});
|
|
134
142
|
|
|
143
|
+
app.get("/v1/workspaces/:workspaceId/document-default-collection-backfills", async (c) => {
|
|
144
|
+
const workspaceId = c.req.param("workspaceId");
|
|
145
|
+
const authorization = await requireAccessGrantAuthorization(
|
|
146
|
+
c,
|
|
147
|
+
deps,
|
|
148
|
+
workspaceId,
|
|
149
|
+
"documents:manage",
|
|
150
|
+
);
|
|
151
|
+
if (!hasAccountAdminAuthority(authorization)) {
|
|
152
|
+
throw new HTTPException(403, { message: "missing permission: account:admin" });
|
|
153
|
+
}
|
|
154
|
+
const query = ListDocumentMigrationAuditQuery.safeParse(c.req.query());
|
|
155
|
+
if (!query.success) {
|
|
156
|
+
throw new HTTPException(400, { message: "invalid document migration audit query" });
|
|
157
|
+
}
|
|
158
|
+
try {
|
|
159
|
+
return c.json(
|
|
160
|
+
ListDocumentDefaultCollectionBackfillRunsResponse.parse(
|
|
161
|
+
await listDocumentDefaultCollectionBackfillRuns(db, {
|
|
162
|
+
accountId: authorization.grant.accountId,
|
|
163
|
+
workspaceId,
|
|
164
|
+
actorSubjectId: authorization.grant.subjectId,
|
|
165
|
+
accountAdminAuthorization: requireAccountAdminAuthorizationStamp(authorization),
|
|
166
|
+
...query.data,
|
|
167
|
+
}),
|
|
168
|
+
),
|
|
169
|
+
);
|
|
170
|
+
} catch (error) {
|
|
171
|
+
throw documentHttpException(error);
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
app.get("/v1/workspaces/:workspaceId/document-default-collection-backfills/:runId", async (c) => {
|
|
176
|
+
const workspaceId = c.req.param("workspaceId");
|
|
177
|
+
const authorization = await requireAccessGrantAuthorization(
|
|
178
|
+
c,
|
|
179
|
+
deps,
|
|
180
|
+
workspaceId,
|
|
181
|
+
"documents:manage",
|
|
182
|
+
);
|
|
183
|
+
if (!hasAccountAdminAuthority(authorization)) {
|
|
184
|
+
throw new HTTPException(403, { message: "missing permission: account:admin" });
|
|
185
|
+
}
|
|
186
|
+
const runId = RunDocumentDefaultCollectionBackfillRequest.shape.runId.safeParse(
|
|
187
|
+
c.req.param("runId"),
|
|
188
|
+
);
|
|
189
|
+
const query = GetDocumentDefaultCollectionBackfillAuditQuery.safeParse(c.req.query());
|
|
190
|
+
if (!runId.success || !query.success) {
|
|
191
|
+
throw new HTTPException(400, { message: "invalid document migration audit query" });
|
|
192
|
+
}
|
|
193
|
+
try {
|
|
194
|
+
return c.json(
|
|
195
|
+
DocumentDefaultCollectionBackfillAudit.parse(
|
|
196
|
+
await getDocumentDefaultCollectionBackfillAudit(db, {
|
|
197
|
+
accountId: authorization.grant.accountId,
|
|
198
|
+
workspaceId,
|
|
199
|
+
actorSubjectId: authorization.grant.subjectId,
|
|
200
|
+
accountAdminAuthorization: requireAccountAdminAuthorizationStamp(authorization),
|
|
201
|
+
runId: runId.data,
|
|
202
|
+
...query.data,
|
|
203
|
+
}),
|
|
204
|
+
),
|
|
205
|
+
);
|
|
206
|
+
} catch (error) {
|
|
207
|
+
throw documentHttpException(error);
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
app.get("/v1/workspaces/:workspaceId/document-authority-reclassifications", async (c) => {
|
|
212
|
+
const workspaceId = c.req.param("workspaceId");
|
|
213
|
+
const authorization = await requireAccessGrantAuthorization(
|
|
214
|
+
c,
|
|
215
|
+
deps,
|
|
216
|
+
workspaceId,
|
|
217
|
+
"documents:manage",
|
|
218
|
+
);
|
|
219
|
+
if (!hasAccountAdminAuthority(authorization)) {
|
|
220
|
+
throw new HTTPException(403, { message: "missing permission: account:admin" });
|
|
221
|
+
}
|
|
222
|
+
const query = ListDocumentMigrationAuditQuery.safeParse(c.req.query());
|
|
223
|
+
if (!query.success) {
|
|
224
|
+
throw new HTTPException(400, { message: "invalid document migration audit query" });
|
|
225
|
+
}
|
|
226
|
+
try {
|
|
227
|
+
return c.json(
|
|
228
|
+
ListOrganizationDocumentAuthorityReclassificationsResponse.parse(
|
|
229
|
+
await listOrganizationDocumentAuthorityReclassifications(db, {
|
|
230
|
+
accountId: authorization.grant.accountId,
|
|
231
|
+
workspaceId,
|
|
232
|
+
actorSubjectId: authorization.grant.subjectId,
|
|
233
|
+
accountAdminAuthorization: requireAccountAdminAuthorizationStamp(authorization),
|
|
234
|
+
...query.data,
|
|
235
|
+
}),
|
|
236
|
+
),
|
|
237
|
+
);
|
|
238
|
+
} catch (error) {
|
|
239
|
+
throw documentHttpException(error);
|
|
240
|
+
}
|
|
241
|
+
});
|
|
242
|
+
|
|
135
243
|
app.post("/v1/workspaces/:workspaceId/document-bases/:baseId/documents", async (c) => {
|
|
136
244
|
const workspaceId = c.req.param("workspaceId");
|
|
137
245
|
const access = await requireAccessGrantAuthorization(c, deps, workspaceId, "documents:manage");
|
|
@@ -486,6 +594,7 @@ export function registerDocumentRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
486
594
|
limit: payload.limit,
|
|
487
595
|
mode: payload.mode,
|
|
488
596
|
sourceKinds: payload.sourceKinds,
|
|
597
|
+
authorityKinds: payload.authorityKinds,
|
|
489
598
|
aclTags: payload.aclTags,
|
|
490
599
|
initiatingSubjectId: grant.subjectId,
|
|
491
600
|
surface: "human",
|
|
@@ -512,6 +621,7 @@ export function registerDocumentRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
512
621
|
limit: payload.limit,
|
|
513
622
|
mode: payload.mode,
|
|
514
623
|
sourceKinds: payload.sourceKinds,
|
|
624
|
+
authorityKinds: payload.authorityKinds,
|
|
515
625
|
aclTags: payload.aclTags,
|
|
516
626
|
initiatingSubjectId: grant.subjectId,
|
|
517
627
|
surface: "human",
|
|
@@ -879,13 +989,16 @@ function dropFilename(preferred: string | undefined): string {
|
|
|
879
989
|
}
|
|
880
990
|
|
|
881
991
|
function documentHttpException(error: unknown): HTTPException {
|
|
882
|
-
const message =
|
|
992
|
+
const message = documentDomainErrorMessage(error);
|
|
883
993
|
if (message.includes("organization document") && message.includes("exact account authority")) {
|
|
884
994
|
return new HTTPException(403, { message: "missing permission: account:admin" });
|
|
885
995
|
}
|
|
886
996
|
if (message.includes("not found")) {
|
|
887
997
|
return new HTTPException(404, { message });
|
|
888
998
|
}
|
|
999
|
+
if (message.includes("document Default collection backfill audit run is unavailable")) {
|
|
1000
|
+
return new HTTPException(404, { message });
|
|
1001
|
+
}
|
|
889
1002
|
if (message.includes("already exists")) {
|
|
890
1003
|
return new HTTPException(409, { message });
|
|
891
1004
|
}
|
|
@@ -898,7 +1011,9 @@ function documentHttpException(error: unknown): HTTPException {
|
|
|
898
1011
|
}
|
|
899
1012
|
if (
|
|
900
1013
|
message.includes("reclassification requires") ||
|
|
901
|
-
message.includes("backfill requires organization administration")
|
|
1014
|
+
message.includes("backfill requires organization administration") ||
|
|
1015
|
+
message.includes("document migration audit requires organization administration") ||
|
|
1016
|
+
message.includes("document migration audit scope is invalid")
|
|
902
1017
|
) {
|
|
903
1018
|
return new HTTPException(403, { message });
|
|
904
1019
|
}
|
|
@@ -906,7 +1021,12 @@ function documentHttpException(error: unknown): HTTPException {
|
|
|
906
1021
|
message.includes("reclassification input is invalid") ||
|
|
907
1022
|
message.includes("reclassification authority is invalid") ||
|
|
908
1023
|
message.includes("invalid document authority receipt cursor") ||
|
|
909
|
-
message.includes("document authority receipt limit")
|
|
1024
|
+
message.includes("document authority receipt limit") ||
|
|
1025
|
+
message.includes("invalid document migration audit cursor") ||
|
|
1026
|
+
message.includes("document migration audit cursor is invalid") ||
|
|
1027
|
+
message.includes("document migration audit limit") ||
|
|
1028
|
+
message.includes("document migration audit authority is incomplete") ||
|
|
1029
|
+
message.includes("document Default collection backfill audit run id is required")
|
|
910
1030
|
) {
|
|
911
1031
|
return new HTTPException(400, { message });
|
|
912
1032
|
}
|
|
@@ -929,6 +1049,19 @@ function documentHttpException(error: unknown): HTTPException {
|
|
|
929
1049
|
return new HTTPException(500, { message });
|
|
930
1050
|
}
|
|
931
1051
|
|
|
1052
|
+
function documentDomainErrorMessage(error: unknown): string {
|
|
1053
|
+
let current: unknown = error;
|
|
1054
|
+
let message = error instanceof Error ? error.message : String(error);
|
|
1055
|
+
const seen = new Set<unknown>();
|
|
1056
|
+
while (current && typeof current === "object" && !seen.has(current) && seen.size < 8) {
|
|
1057
|
+
seen.add(current);
|
|
1058
|
+
const record = current as { cause?: unknown; message?: unknown };
|
|
1059
|
+
if (typeof record.message === "string") message = record.message;
|
|
1060
|
+
current = record.cause;
|
|
1061
|
+
}
|
|
1062
|
+
return message;
|
|
1063
|
+
}
|
|
1064
|
+
|
|
932
1065
|
function hasAccountAdminAuthority(
|
|
933
1066
|
authorization: Awaited<ReturnType<typeof requireAccessGrantAuthorization>>,
|
|
934
1067
|
): boolean {
|