@opengeni/api-router 2.11.0-canary.0 → 2.11.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/{chunk-EAYCECQK.js → chunk-5X3XM6CE.js} +1841 -1528
- package/dist/chunk-5X3XM6CE.js.map +1 -0
- package/dist/http/sse.d.ts +4 -3
- package/dist/index.js +5 -3
- package/dist/index.js.map +1 -1
- package/dist/integrations/github-skill-source.d.ts +1 -5
- package/dist/mcp/remember.d.ts +1 -1
- package/dist/mcp/scheduled-task-view.d.ts +3 -3
- package/dist/mcp/server.d.ts +4 -4
- package/dist/routes/skill-content.d.ts +4 -0
- package/dist/routes/skill-install-authority.d.ts +6 -0
- package/dist/transcription/providers/xai-subscription.d.ts +1 -0
- package/dist/transcription/service.d.ts +2 -1
- package/package.json +19 -19
- package/src/app.ts +18 -0
- package/src/http/sse.ts +9 -25
- package/src/index.ts +3 -1
- package/src/integrations/github-skill-source.ts +2 -142
- package/src/mcp/company-brain-governed-writes.ts +15 -21
- package/src/mcp/remember.ts +14 -15
- package/src/mcp/server.ts +7 -4
- package/src/routes/automations.ts +35 -0
- package/src/routes/packs.ts +61 -8
- package/src/routes/plugins.ts +48 -5
- package/src/routes/preference-registry.ts +12 -93
- package/src/routes/sessions.ts +52 -20
- package/src/routes/skill-content.ts +226 -0
- package/src/routes/skill-install-authority.ts +64 -0
- package/src/routes/skills.ts +38 -3
- package/src/routes/transcription-recordings.ts +30 -1
- package/src/routes/transcriptions.ts +4 -0
- package/src/transcription/providers/codex-subscription.ts +2 -0
- package/src/transcription/providers/openai.ts +1 -0
- package/src/transcription/providers/xai-subscription.ts +49 -3
- package/src/transcription/service.ts +43 -6
- package/src/xai-subscription-auth.ts +9 -2
- package/dist/chunk-EAYCECQK.js.map +0 -1
package/src/mcp/remember.ts
CHANGED
|
@@ -59,7 +59,7 @@ const laneFields = {
|
|
|
59
59
|
/**
|
|
60
60
|
* Explicit user-directed remember. Content becomes exact task-note evidence,
|
|
61
61
|
* promotion runs through the frozen workspace learning policy, and activation
|
|
62
|
-
* is either automatic (
|
|
62
|
+
* is either automatic (instruction under Automatic mode) or requires exactly one
|
|
63
63
|
* bound human confirmation through the built-in `request_human_input` tool.
|
|
64
64
|
* Tool input cannot select scope beyond the workspace, active authority, or
|
|
65
65
|
* another learning-policy source.
|
|
@@ -70,9 +70,9 @@ export function registerRememberTools(input: RegisterRememberToolsInput): void {
|
|
|
70
70
|
"remember",
|
|
71
71
|
{
|
|
72
72
|
description:
|
|
73
|
-
"Create governed durable knowledge
|
|
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.
|
|
75
|
-
"Under Autonomous learning, an eligible
|
|
73
|
+
"Create governed durable knowledge or a mandatory workspace instruction. Skills now use skill_save; the legacy preference lane returns a redirect without writing. When memory_save is available, use it for ordinary durable facts, decisions, incidents, bug fixes, and confirmed outcomes; Memory writes are autonomous and independent of Learning mode. Use lane=knowledge only when memory_save is unavailable and the user explicitly requests reviewed workspace knowledge; 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. Do not copy one item into multiple lanes. ` +
|
|
75
|
+
"Under Autonomous learning, an eligible workspace instruction may activate immediately through its governed lifecycle. Under Require approval, it remains inactive and 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. Off creates no governed change. Reviewed Knowledge always needs confirmation. Do not use lane=knowledge for facts you merely inferred; use memory_save when available, otherwise knowledge_propose or task notes. 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,
|
|
@@ -97,6 +97,13 @@ export function registerRememberTools(input: RegisterRememberToolsInput): void {
|
|
|
97
97
|
async (request) => {
|
|
98
98
|
await input.authorize();
|
|
99
99
|
const { lane, operationId, content, reason } = request;
|
|
100
|
+
if (lane === "preference")
|
|
101
|
+
return input.json({
|
|
102
|
+
status: "not_remembered",
|
|
103
|
+
code: "use_skill_save",
|
|
104
|
+
message:
|
|
105
|
+
"Skills use the shared file lifecycle. Read opengeni-skills with skill_read, then discover skill_save. No Knowledge evidence or confirmation question is needed for Skill edits.",
|
|
106
|
+
});
|
|
100
107
|
if (lane !== "knowledge" && content.length > laneContentMaxChars[lane]) {
|
|
101
108
|
return input.json({
|
|
102
109
|
status: "not_remembered",
|
|
@@ -109,17 +116,9 @@ export function registerRememberTools(input: RegisterRememberToolsInput): void {
|
|
|
109
116
|
}
|
|
110
117
|
const base = { operationId, content, reason, scope: "workspace" as const };
|
|
111
118
|
const rememberRequest =
|
|
112
|
-
lane === "
|
|
113
|
-
? {
|
|
114
|
-
|
|
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) };
|
|
119
|
+
lane === "instruction_policy"
|
|
120
|
+
? { ...base, lane, target: request.target }
|
|
121
|
+
: { ...base, lane, subject: request.subject ?? content.slice(0, 80) };
|
|
123
122
|
try {
|
|
124
123
|
return input.json(
|
|
125
124
|
await router.remember({ attempt: input.attempt, request: rememberRequest }),
|
package/src/mcp/server.ts
CHANGED
|
@@ -2587,7 +2587,9 @@ function registerGoalTools(
|
|
|
2587
2587
|
description:
|
|
2588
2588
|
"End the current turn and wait out of turn for relevant session input. This is self-only and does not require a goal. After success, the production runtime ends the turn at the tool-batch boundary without another model step or final message. Use it for long or uncertain waits instead of sleeping or repeatedly calling session_wait/command_wait. timeoutSeconds is a relative safety-wake duration; OpenGeni persists the first absolute deadline for the turn, and repeated calls do not extend it. Timeout never cancels a background command. A human/API prompt, agent message or Steer, child terminal result, scheduled input, terminal background-command result, or the deadline wakes the session. Use goal_pause instead when the active goal itself should stop pending a human decision.",
|
|
2589
2589
|
inputSchema: {
|
|
2590
|
-
reason: inputWaitReasonSchema
|
|
2590
|
+
reason: inputWaitReasonSchema.describe(
|
|
2591
|
+
"Shown directly to the user. Write one short, natural sentence explaining what you are waiting for, with normal spacing. Exclude internal IDs, cursors, commit hashes, paths, and continuation instructions. Example: Waiting for the build and database checks to finish.",
|
|
2592
|
+
),
|
|
2591
2593
|
timeoutSeconds: z4
|
|
2592
2594
|
.number()
|
|
2593
2595
|
.int()
|
|
@@ -4665,7 +4667,7 @@ function registerWorkspaceOrchestrationTools(
|
|
|
4665
4667
|
"session_get",
|
|
4666
4668
|
{
|
|
4667
4669
|
description:
|
|
4668
|
-
"Get an authorized session. Omit sessionId to read only the authenticated current agent session (a child reads itself, never its parent or root); sessionless/operator callers must provide an explicit ID. Both forms retain live-attempt and target authorization checks. Default detail=compact returns status, goal (including completion evidence or pause rationale), latest recorded goal progress, meaningful pause/wait state, queue counts, active turn and snapshot lastSequence. Goal completion is not a terminal child result: join with session_wait waitFor=completion from the last consumed event cursor (0 if none), not this snapshot lastSequence, which may already include an unread completion. For an already-settled child, retrieve its result-bearing completion with session_events or join from the last consumed cursor. Use detail=full for the legacy bounded configuration including resources, persisted tool refs, effectiveToolPolicy and variableSet ids (never variable values). Full mode is configuration, not a substitute for compact goal/progress facts. Self reads inspect session state, not conversation history, which is supplied directly. Text loss is explicit; REST/UI defaults are unchanged.",
|
|
4670
|
+
"Get an authorized session. Omit sessionId to read only the authenticated current agent session (a child reads itself, never its parent or root); sessionless/operator callers must provide an explicit ID. Both forms retain live-attempt and target authorization checks. Default detail=compact returns status, goal (including completion evidence or pause rationale), latest recorded goal progress, meaningful pause/wait state, queue counts, active turn and snapshot lastSequence. Queued status and updatedAt are not proof of execution; inspect the active turn and durable results. Goal completion is not a terminal child result: join with session_wait waitFor=completion from the last consumed event cursor (0 if none), not this snapshot lastSequence, which may already include an unread completion. For an already-settled child, retrieve its result-bearing completion with session_events or join from the last consumed cursor. Use detail=full for the legacy bounded configuration including resources, persisted tool refs, effectiveToolPolicy and variableSet ids (never variable values). Full mode is configuration, not a substitute for compact goal/progress facts. Self reads inspect session state, not conversation history, which is supplied directly. Text loss is explicit; REST/UI defaults are unchanged.",
|
|
4669
4671
|
inputSchema: {
|
|
4670
4672
|
sessionId: z4
|
|
4671
4673
|
.string()
|
|
@@ -5241,7 +5243,7 @@ function registerWorkspaceOrchestrationTools(
|
|
|
5241
5243
|
"session_send_message",
|
|
5242
5244
|
{
|
|
5243
5245
|
description:
|
|
5244
|
-
"
|
|
5246
|
+
"Acceptance is not execution. Keep resource.id: for agent messages, match that ID in payload.updateIds from session_events view=debug, includeTypes=[system.update.delivered], payloadMode=full; retain the event turnId and read its relevant result. An unrelated in-flight turn completing does not prove delivery. Do not resend an unconsumed message; inspect blockers. Worker messages are coalescible machine input, added to history when claimed. Sessionless operator calls append a human/API prompt and resource.id is its turn ID. Use your last consumed event sequence. Report stalled delivery if it cannot safely progress.",
|
|
5245
5247
|
inputSchema: {
|
|
5246
5248
|
sessionId: z4.string().uuid(),
|
|
5247
5249
|
text: z4.string().min(1),
|
|
@@ -5343,7 +5345,8 @@ function registerWorkspaceOrchestrationTools(
|
|
|
5343
5345
|
server.registerTool(
|
|
5344
5346
|
"session_pause",
|
|
5345
5347
|
{
|
|
5346
|
-
description:
|
|
5348
|
+
description:
|
|
5349
|
+
"Pause the selected session workstream, including descendants. From an agent, pausing an ancestor also stops this caller; it cannot then issue its own Resume. Do not use ancestor Pause merely to prevent concurrent edits. Waiting prompts stay saved and inert until Resume.",
|
|
5347
5350
|
inputSchema: {
|
|
5348
5351
|
sessionId: z4.string().uuid(),
|
|
5349
5352
|
idempotencyKey: z4.string().uuid(),
|
|
@@ -10,6 +10,9 @@ import {
|
|
|
10
10
|
UpdateAutomationSourceRequest,
|
|
11
11
|
UpdateAutomationTriggerRequest,
|
|
12
12
|
stableJson,
|
|
13
|
+
resolveBundledSkillSelection,
|
|
14
|
+
type AccessGrant,
|
|
15
|
+
type BundledSkillId,
|
|
13
16
|
} from "@opengeni/contracts";
|
|
14
17
|
import {
|
|
15
18
|
AutomationDeliveryConflictError,
|
|
@@ -21,6 +24,7 @@ import {
|
|
|
21
24
|
encryptVariableSetValue,
|
|
22
25
|
getAutomationSourceSecret,
|
|
23
26
|
getAutomationTriggerRevisions,
|
|
27
|
+
getSession,
|
|
24
28
|
listActiveAutomationTriggersForSource,
|
|
25
29
|
listAutomationRuns,
|
|
26
30
|
listAutomationSources,
|
|
@@ -38,6 +42,7 @@ import {
|
|
|
38
42
|
assertWorkspaceModelPolicyAllows,
|
|
39
43
|
buildAutomationAcceptedExecution,
|
|
40
44
|
canonicalConfiguredModel,
|
|
45
|
+
creationInitiatorForGrant,
|
|
41
46
|
workspaceCustomModelReference,
|
|
42
47
|
lockActiveCustomModelForAdmission,
|
|
43
48
|
requireAccessGrant,
|
|
@@ -49,6 +54,26 @@ import {
|
|
|
49
54
|
import type { Hono } from "hono";
|
|
50
55
|
import { HTTPException } from "hono/http-exception";
|
|
51
56
|
|
|
57
|
+
async function callerBundleSelection(
|
|
58
|
+
deps: ApiRouteDeps,
|
|
59
|
+
grant: AccessGrant,
|
|
60
|
+
requested: BundledSkillId[] | undefined,
|
|
61
|
+
) {
|
|
62
|
+
const actor = creationInitiatorForGrant(grant).actor;
|
|
63
|
+
const parent = actor ? await getSession(deps.db, grant.workspaceId, actor.sessionId) : null;
|
|
64
|
+
if (actor && (!parent || parent.accountId !== grant.accountId))
|
|
65
|
+
throw new HTTPException(403, {
|
|
66
|
+
message: "Automation Skill selection requires the creating agent's session",
|
|
67
|
+
});
|
|
68
|
+
try {
|
|
69
|
+
return resolveBundledSkillSelection(requested, parent?.bundledSkillIds);
|
|
70
|
+
} catch (error) {
|
|
71
|
+
throw new HTTPException(422, {
|
|
72
|
+
message: error instanceof Error ? error.message : "Invalid bundled Skill selection",
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
52
77
|
export function registerAutomationRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
53
78
|
app.get("/v1/workspaces/:workspaceId/automations/sources", async (c) => {
|
|
54
79
|
const workspaceId = c.req.param("workspaceId");
|
|
@@ -146,6 +171,11 @@ export function registerAutomationRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
146
171
|
const workspaceId = c.req.param("workspaceId");
|
|
147
172
|
const grant = await requireAccessGrant(c, deps, workspaceId, "workspace:admin");
|
|
148
173
|
const request = CreateAutomationTriggerRequest.parse(await c.req.json());
|
|
174
|
+
request.sessionTemplate.bundledSkillIds = await callerBundleSelection(
|
|
175
|
+
deps,
|
|
176
|
+
grant,
|
|
177
|
+
request.sessionTemplate.bundledSkillIds,
|
|
178
|
+
);
|
|
149
179
|
if (request.packInstallationId || request.packTemplateId) {
|
|
150
180
|
throw new HTTPException(409, {
|
|
151
181
|
message: "Pack-owned automation triggers must be created through their Pack setup API",
|
|
@@ -220,6 +250,11 @@ export function registerAutomationRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
220
250
|
requireAutomationAdapter(existing.adapterId).validateTriggerParameters(request.parameters);
|
|
221
251
|
}
|
|
222
252
|
if (request.sessionTemplate) {
|
|
253
|
+
request.sessionTemplate.bundledSkillIds = await callerBundleSelection(
|
|
254
|
+
deps,
|
|
255
|
+
grant,
|
|
256
|
+
request.sessionTemplate.bundledSkillIds,
|
|
257
|
+
);
|
|
223
258
|
for (const permission of request.sessionTemplate.firstPartyMcpPermissions) {
|
|
224
259
|
requirePermission(grant, permission);
|
|
225
260
|
}
|
package/src/routes/packs.ts
CHANGED
|
@@ -29,6 +29,7 @@ import {
|
|
|
29
29
|
getWorkspacePack,
|
|
30
30
|
getSocialConnection,
|
|
31
31
|
installPortableSkill,
|
|
32
|
+
SkillSourceRemovalAuthorityError,
|
|
32
33
|
listPackInstallations,
|
|
33
34
|
listSocialConnections,
|
|
34
35
|
PackManifestChangedError,
|
|
@@ -50,7 +51,8 @@ import {
|
|
|
50
51
|
import { getDocumentBase } from "@opengeni/documents";
|
|
51
52
|
import type { Hono } from "hono";
|
|
52
53
|
import { HTTPException } from "hono/http-exception";
|
|
53
|
-
import { requireAccessGrant } from "@opengeni/core";
|
|
54
|
+
import { requireAccessGrant, requireAccessGrantAuthorization } from "@opengeni/core";
|
|
55
|
+
import { skillInstallerActor, skillRemovalActor } from "./skill-install-authority";
|
|
54
56
|
import { requireLimit } from "@opengeni/core";
|
|
55
57
|
import type { ApiRouteDeps } from "@opengeni/core";
|
|
56
58
|
import { validateVariableSetAttachment } from "@opengeni/core";
|
|
@@ -172,8 +174,15 @@ export function registerPackRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
172
174
|
|
|
173
175
|
app.post("/v1/workspaces/:workspaceId/packs/:packId/install", async (c) => {
|
|
174
176
|
const workspaceId = c.req.param("workspaceId");
|
|
175
|
-
const
|
|
177
|
+
const access = await requireAccessGrantAuthorization(
|
|
178
|
+
c,
|
|
179
|
+
deps,
|
|
180
|
+
workspaceId,
|
|
181
|
+
"capabilities:manage",
|
|
182
|
+
);
|
|
183
|
+
const { grant } = access;
|
|
176
184
|
const pack = await requirePack(db, workspaceId, c.req.param("packId"));
|
|
185
|
+
const skillActor = pack.skills.length ? skillInstallerActor(access) : undefined;
|
|
177
186
|
const payload = InstallPackRequest.parse(await c.req.json());
|
|
178
187
|
const preview = PackInstallationPreview.parse(
|
|
179
188
|
await previewCapabilityPackInstallation(db, grant, pack, {
|
|
@@ -245,10 +254,22 @@ export function registerPackRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
245
254
|
message: "The completed Pack operation lost its installation record",
|
|
246
255
|
});
|
|
247
256
|
}
|
|
248
|
-
return c.json(
|
|
257
|
+
return c.json(
|
|
258
|
+
PackInstallation.parse({
|
|
259
|
+
...replayed,
|
|
260
|
+
...(Array.isArray(prepared.replayResult.skillWrites)
|
|
261
|
+
? { skillWrites: prepared.replayResult.skillWrites }
|
|
262
|
+
: {}),
|
|
263
|
+
...(Array.isArray(prepared.replayResult.skillReleases)
|
|
264
|
+
? { skillReleases: prepared.replayResult.skillReleases }
|
|
265
|
+
: {}),
|
|
266
|
+
}),
|
|
267
|
+
200,
|
|
268
|
+
);
|
|
249
269
|
}
|
|
250
270
|
|
|
251
271
|
const retainedComponentKeys: string[] = [];
|
|
272
|
+
const skillWrites: import("@opengeni/contracts").SkillWriteReceipt[] = [];
|
|
252
273
|
const retainedFacetInstallationIds: string[] = [];
|
|
253
274
|
const retainedBindingIds: string[] = [];
|
|
254
275
|
let activeComponentKey = "manifest";
|
|
@@ -261,10 +282,13 @@ export function registerPackRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
261
282
|
});
|
|
262
283
|
try {
|
|
263
284
|
for (const skill of pack.skills) {
|
|
285
|
+
if (!skillActor)
|
|
286
|
+
throw new HTTPException(403, { message: "Skill installer authority is missing" });
|
|
264
287
|
await heartbeat();
|
|
265
288
|
const inline = inlinePackSkillInstall(pack, skill);
|
|
266
289
|
activeComponentKey = inline.componentKey;
|
|
267
290
|
const installed = await installPortableSkill(db, {
|
|
291
|
+
skillActor,
|
|
268
292
|
accountId: grant.accountId,
|
|
269
293
|
workspaceId,
|
|
270
294
|
subjectId: grant.subjectId,
|
|
@@ -287,6 +311,7 @@ export function registerPackRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
287
311
|
removable: true,
|
|
288
312
|
},
|
|
289
313
|
});
|
|
314
|
+
skillWrites.push(installed.skillReceipt);
|
|
290
315
|
await heartbeat();
|
|
291
316
|
await recordPackInlineSkillComponent(db, {
|
|
292
317
|
accountId: grant.accountId,
|
|
@@ -316,7 +341,9 @@ export function registerPackRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
316
341
|
|
|
317
342
|
activeComponentKey = "ownership-finalize";
|
|
318
343
|
await heartbeat();
|
|
319
|
-
|
|
344
|
+
const removalActor = skillRemovalActor(access);
|
|
345
|
+
const { skillReleases } = await finalizePackComponentOwnership(db, {
|
|
346
|
+
...(removalActor ? { skillActor: removalActor } : {}),
|
|
320
347
|
accountId: grant.accountId,
|
|
321
348
|
workspaceId,
|
|
322
349
|
packInstallationId: prepared.installation.id,
|
|
@@ -334,13 +361,19 @@ export function registerPackRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
334
361
|
packId: pack.id,
|
|
335
362
|
result: {
|
|
336
363
|
status: "installed",
|
|
364
|
+
...(skillWrites.length ? { skillWrites } : {}),
|
|
337
365
|
packId: pack.id,
|
|
338
366
|
manifestDigest: preview.manifestDigest,
|
|
339
367
|
componentCount: retainedComponentKeys.length,
|
|
368
|
+
...(skillReleases.length ? { skillReleases } : {}),
|
|
340
369
|
},
|
|
341
370
|
});
|
|
342
371
|
return c.json(
|
|
343
|
-
PackInstallation.parse(
|
|
372
|
+
PackInstallation.parse({
|
|
373
|
+
...finalized,
|
|
374
|
+
...(skillWrites.length ? { skillWrites } : {}),
|
|
375
|
+
...(skillReleases.length ? { skillReleases } : {}),
|
|
376
|
+
}),
|
|
344
377
|
preview.installationVersion === null ? 201 : 200,
|
|
345
378
|
);
|
|
346
379
|
} catch (error) {
|
|
@@ -363,7 +396,10 @@ export function registerPackRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
363
396
|
message: `Pack component ${activeComponentKey} is pinned by another installed owner. Resolve that version conflict and retry with the same idempotency key.`,
|
|
364
397
|
});
|
|
365
398
|
}
|
|
366
|
-
if (
|
|
399
|
+
if (
|
|
400
|
+
error instanceof PackOperationClaimLostError ||
|
|
401
|
+
error instanceof SkillSourceRemovalAuthorityError
|
|
402
|
+
) {
|
|
367
403
|
throw packMutationHttpError(error);
|
|
368
404
|
}
|
|
369
405
|
if (error instanceof HTTPException) throw error;
|
|
@@ -394,7 +430,14 @@ export function registerPackRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
394
430
|
|
|
395
431
|
app.delete("/v1/workspaces/:workspaceId/packs/:packId/installation", async (c) => {
|
|
396
432
|
const workspaceId = c.req.param("workspaceId");
|
|
397
|
-
const
|
|
433
|
+
const access = await requireAccessGrantAuthorization(
|
|
434
|
+
c,
|
|
435
|
+
deps,
|
|
436
|
+
workspaceId,
|
|
437
|
+
"capabilities:manage",
|
|
438
|
+
);
|
|
439
|
+
const { grant } = access;
|
|
440
|
+
const skillActor = skillRemovalActor(access);
|
|
398
441
|
const pack = await requirePack(db, workspaceId, c.req.param("packId"));
|
|
399
442
|
const payload = UninstallPackRequest.parse(await c.req.json());
|
|
400
443
|
const requestDigest = sha256(
|
|
@@ -426,6 +469,9 @@ export function registerPackRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
426
469
|
retainedComponents: Array.isArray(prepared.replayResult.retainedComponents)
|
|
427
470
|
? prepared.replayResult.retainedComponents
|
|
428
471
|
: [],
|
|
472
|
+
...(Array.isArray(prepared.replayResult.skillReleases)
|
|
473
|
+
? { skillReleases: prepared.replayResult.skillReleases }
|
|
474
|
+
: {}),
|
|
429
475
|
}),
|
|
430
476
|
);
|
|
431
477
|
}
|
|
@@ -437,6 +483,7 @@ export function registerPackRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
437
483
|
operationVersion: prepared.operationVersion,
|
|
438
484
|
});
|
|
439
485
|
const released = await releasePackComponents(db, {
|
|
486
|
+
...(skillActor ? { skillActor } : {}),
|
|
440
487
|
accountId: grant.accountId,
|
|
441
488
|
workspaceId,
|
|
442
489
|
packInstallationId: prepared.installation.id,
|
|
@@ -451,6 +498,7 @@ export function registerPackRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
451
498
|
packId: pack.id,
|
|
452
499
|
status: "uninstalled",
|
|
453
500
|
retainedComponents: [...new Set(released.retainedComponents)],
|
|
501
|
+
...(released.skillReleases?.length ? { skillReleases: released.skillReleases } : {}),
|
|
454
502
|
});
|
|
455
503
|
await finalizePackUninstallOperation(db, {
|
|
456
504
|
accountId: grant.accountId,
|
|
@@ -472,7 +520,10 @@ export function registerPackRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
472
520
|
phase: "uninstall_failed",
|
|
473
521
|
errorCode: packFailureCode(error),
|
|
474
522
|
}).catch(() => undefined);
|
|
475
|
-
if (
|
|
523
|
+
if (
|
|
524
|
+
error instanceof PackOperationClaimLostError ||
|
|
525
|
+
error instanceof SkillSourceRemovalAuthorityError
|
|
526
|
+
) {
|
|
476
527
|
throw packMutationHttpError(error);
|
|
477
528
|
}
|
|
478
529
|
if (error instanceof HTTPException) throw error;
|
|
@@ -628,6 +679,8 @@ export function registerPackRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
628
679
|
}
|
|
629
680
|
|
|
630
681
|
function packMutationHttpError(error: unknown): HTTPException {
|
|
682
|
+
if (error instanceof SkillSourceRemovalAuthorityError)
|
|
683
|
+
return new HTTPException(403, { message: error.message });
|
|
631
684
|
if (error instanceof PackManifestChangedError) {
|
|
632
685
|
return new HTTPException(409, {
|
|
633
686
|
message: "The Pack manifest changed after preview. Review the updated installation plan.",
|
package/src/routes/plugins.ts
CHANGED
|
@@ -18,15 +18,18 @@ import {
|
|
|
18
18
|
UninstallPluginResult,
|
|
19
19
|
stableJson,
|
|
20
20
|
type PluginComponentPreview,
|
|
21
|
+
type SkillActor,
|
|
21
22
|
} from "@opengeni/contracts";
|
|
22
23
|
import {
|
|
23
24
|
portableSkillCapabilityId,
|
|
24
25
|
portableSkillPluginKey,
|
|
25
26
|
requireAccessGrant,
|
|
27
|
+
requireAccessGrantAuthorization,
|
|
26
28
|
resolveSkillImport,
|
|
27
29
|
type ApiRouteDeps,
|
|
28
30
|
type GitHubSkillSourceClient,
|
|
29
31
|
} from "@opengeni/core";
|
|
32
|
+
import { skillInstallerActor, skillRemovalActor } from "./skill-install-authority";
|
|
30
33
|
import {
|
|
31
34
|
buildConnectionTokenResolver,
|
|
32
35
|
CapabilityComponentVersionConflictError,
|
|
@@ -40,6 +43,7 @@ import {
|
|
|
40
43
|
installApiIntegration,
|
|
41
44
|
installPluginMcpReference,
|
|
42
45
|
installPortableSkill,
|
|
46
|
+
SkillSourceRemovalAuthorityError,
|
|
43
47
|
listInstalledPluginPackages,
|
|
44
48
|
PluginInstallationVersionConflictError,
|
|
45
49
|
PluginInstallationVersionRequiredError,
|
|
@@ -71,6 +75,7 @@ type ResolvedPluginComponent = {
|
|
|
71
75
|
install(ownerPluginInstallationId: string): Promise<{
|
|
72
76
|
facetInstallationIds: string[];
|
|
73
77
|
bindingIds?: string[];
|
|
78
|
+
skillReceipt?: import("@opengeni/contracts").SkillWriteReceipt;
|
|
74
79
|
}>;
|
|
75
80
|
};
|
|
76
81
|
|
|
@@ -120,9 +125,16 @@ export function registerPluginRoutes(
|
|
|
120
125
|
|
|
121
126
|
app.post("/v1/workspaces/:workspaceId/plugins/install", async (c) => {
|
|
122
127
|
const workspaceId = c.req.param("workspaceId");
|
|
123
|
-
const
|
|
128
|
+
const access = await requireAccessGrantAuthorization(
|
|
129
|
+
c,
|
|
130
|
+
deps,
|
|
131
|
+
workspaceId,
|
|
132
|
+
"capabilities:manage",
|
|
133
|
+
);
|
|
134
|
+
const { grant } = access;
|
|
124
135
|
const payload = InstallPluginRequest.parse(await c.req.json());
|
|
125
136
|
const resolved = await resolvePluginPackage({
|
|
137
|
+
skillActor: () => skillInstallerActor(access),
|
|
126
138
|
deps,
|
|
127
139
|
github,
|
|
128
140
|
transport,
|
|
@@ -188,11 +200,13 @@ export function registerPluginRoutes(
|
|
|
188
200
|
const retainedFacetInstallationIds: string[] = [];
|
|
189
201
|
const retainedBindingIds: string[] = [];
|
|
190
202
|
const completedKeys: string[] = [];
|
|
203
|
+
const skillWrites: import("@opengeni/contracts").SkillWriteReceipt[] = [];
|
|
191
204
|
let activeComponentKey = "none";
|
|
192
205
|
try {
|
|
193
206
|
for (const component of resolved.components) {
|
|
194
207
|
activeComponentKey = component.preview.key;
|
|
195
208
|
const installed = await component.install(prepared.pluginInstallationId);
|
|
209
|
+
if (installed.skillReceipt) skillWrites.push(installed.skillReceipt);
|
|
196
210
|
retainedFacetInstallationIds.push(...installed.facetInstallationIds);
|
|
197
211
|
retainedBindingIds.push(...(installed.bindingIds ?? []));
|
|
198
212
|
completedKeys.push(component.preview.key);
|
|
@@ -204,6 +218,7 @@ export function registerPluginRoutes(
|
|
|
204
218
|
});
|
|
205
219
|
}
|
|
206
220
|
const result = InstalledPlugin.parse({
|
|
221
|
+
...(skillWrites.length ? { skillWrites } : {}),
|
|
207
222
|
pluginKey: prepared.pluginKey,
|
|
208
223
|
version: prepared.version,
|
|
209
224
|
pluginId: prepared.pluginId,
|
|
@@ -213,7 +228,9 @@ export function registerPluginRoutes(
|
|
|
213
228
|
componentCount: resolved.components.length,
|
|
214
229
|
status: "installed",
|
|
215
230
|
});
|
|
216
|
-
|
|
231
|
+
const removalActor = skillRemovalActor(access);
|
|
232
|
+
const { skillReleases, skillPublications } = await finalizePluginPackageInstall(deps.db, {
|
|
233
|
+
...(removalActor ? { skillActor: removalActor } : {}),
|
|
217
234
|
accountId: grant.accountId,
|
|
218
235
|
workspaceId,
|
|
219
236
|
subjectId: grant.subjectId,
|
|
@@ -223,7 +240,14 @@ export function registerPluginRoutes(
|
|
|
223
240
|
retainedBindingIds,
|
|
224
241
|
result,
|
|
225
242
|
});
|
|
226
|
-
return c.json(
|
|
243
|
+
return c.json(
|
|
244
|
+
{
|
|
245
|
+
...result,
|
|
246
|
+
...(skillReleases.length ? { skillReleases } : {}),
|
|
247
|
+
...(skillPublications.length ? { skillPublications } : {}),
|
|
248
|
+
},
|
|
249
|
+
resolved.preview.installed ? 200 : 201,
|
|
250
|
+
);
|
|
227
251
|
} catch (error) {
|
|
228
252
|
await deferPluginPackageOperation(deps.db, {
|
|
229
253
|
workspaceId,
|
|
@@ -231,6 +255,8 @@ export function registerPluginRoutes(
|
|
|
231
255
|
phase: `component_failed:${activeComponentKey}`,
|
|
232
256
|
errorCode: pluginFailureCode(error),
|
|
233
257
|
}).catch(() => undefined);
|
|
258
|
+
if (error instanceof SkillSourceRemovalAuthorityError)
|
|
259
|
+
throw new HTTPException(403, { message: error.message });
|
|
234
260
|
if (error instanceof HTTPException) throw error;
|
|
235
261
|
if (error instanceof CapabilityComponentVersionConflictError) {
|
|
236
262
|
throw new HTTPException(409, {
|
|
@@ -253,7 +279,14 @@ export function registerPluginRoutes(
|
|
|
253
279
|
|
|
254
280
|
app.delete("/v1/workspaces/:workspaceId/plugins/:pluginKey", async (c) => {
|
|
255
281
|
const workspaceId = c.req.param("workspaceId");
|
|
256
|
-
const
|
|
282
|
+
const access = await requireAccessGrantAuthorization(
|
|
283
|
+
c,
|
|
284
|
+
deps,
|
|
285
|
+
workspaceId,
|
|
286
|
+
"capabilities:manage",
|
|
287
|
+
);
|
|
288
|
+
const { grant } = access;
|
|
289
|
+
const skillActor = skillRemovalActor(access);
|
|
257
290
|
const pluginKey = decodeURIComponent(c.req.param("pluginKey"));
|
|
258
291
|
const payload = UninstallPluginRequest.parse(await c.req.json());
|
|
259
292
|
try {
|
|
@@ -261,6 +294,7 @@ export function registerPluginRoutes(
|
|
|
261
294
|
UninstallPluginResult.parse({
|
|
262
295
|
pluginKey,
|
|
263
296
|
...(await uninstallPluginPackage(deps.db, {
|
|
297
|
+
...(skillActor ? { skillActor } : {}),
|
|
264
298
|
accountId: grant.accountId,
|
|
265
299
|
workspaceId,
|
|
266
300
|
subjectId: grant.subjectId,
|
|
@@ -283,6 +317,7 @@ async function resolvePluginPackage(input: {
|
|
|
283
317
|
accountId: string;
|
|
284
318
|
workspaceId: string;
|
|
285
319
|
subjectId: string;
|
|
320
|
+
skillActor?: () => SkillActor;
|
|
286
321
|
url: string;
|
|
287
322
|
bindings: Record<
|
|
288
323
|
string,
|
|
@@ -342,10 +377,13 @@ async function resolvePluginPackage(input: {
|
|
|
342
377
|
},
|
|
343
378
|
},
|
|
344
379
|
install: async (ownerPluginInstallationId) => {
|
|
380
|
+
if (!input.skillActor)
|
|
381
|
+
throw new HTTPException(403, { message: "Skill installer authority is missing" });
|
|
345
382
|
const summaries = new Map(
|
|
346
383
|
resolved.preview.files.map((file) => [file.path, file] as const),
|
|
347
384
|
);
|
|
348
385
|
const installed = await installPortableSkill(input.deps.db, {
|
|
386
|
+
skillActor: input.skillActor(),
|
|
349
387
|
accountId: input.accountId,
|
|
350
388
|
workspaceId: input.workspaceId,
|
|
351
389
|
subjectId: input.subjectId,
|
|
@@ -366,7 +404,10 @@ async function resolvePluginPackage(input: {
|
|
|
366
404
|
}),
|
|
367
405
|
owner: { kind: "plugin", id: ownerPluginInstallationId, removable: true },
|
|
368
406
|
});
|
|
369
|
-
return {
|
|
407
|
+
return {
|
|
408
|
+
facetInstallationIds: [installed.facetInstallationId],
|
|
409
|
+
skillReceipt: installed.skillReceipt,
|
|
410
|
+
};
|
|
370
411
|
},
|
|
371
412
|
});
|
|
372
413
|
continue;
|
|
@@ -725,6 +766,8 @@ function pluginDiff(
|
|
|
725
766
|
}
|
|
726
767
|
|
|
727
768
|
function pluginMutationHttpError(error: unknown): HTTPException {
|
|
769
|
+
if (error instanceof SkillSourceRemovalAuthorityError)
|
|
770
|
+
return new HTTPException(403, { message: error.message });
|
|
728
771
|
if (error instanceof PluginOperationIdempotencyError) {
|
|
729
772
|
return new HTTPException(409, { message: "Plugin idempotency key was already used" });
|
|
730
773
|
}
|