@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
|
@@ -1,8 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
|
-
ActivatePreferenceRegistryRevisionRequest,
|
|
3
2
|
ChangePreferenceRegistryScopeRequest,
|
|
4
|
-
CorrectPreferenceRegistryRequest,
|
|
5
|
-
CreatePreferenceRegistryProposalRequest,
|
|
6
3
|
DeactivatePreferenceRegistryRequest,
|
|
7
4
|
PreferenceRegistryConflictResponse,
|
|
8
5
|
PreferenceRegistryDetailResponse,
|
|
@@ -11,7 +8,6 @@ import {
|
|
|
11
8
|
PreferenceRegistryListQuery,
|
|
12
9
|
PreferenceRegistryListResponse,
|
|
13
10
|
PreferenceRegistryMutationResponse,
|
|
14
|
-
PreferenceRegistryRecord,
|
|
15
11
|
PreferenceRegistrySnapshot,
|
|
16
12
|
RejectPreferenceRegistryProposalRequest,
|
|
17
13
|
SupersedePreferenceRegistryRequest,
|
|
@@ -26,10 +22,7 @@ import {
|
|
|
26
22
|
type ApiRouteDeps,
|
|
27
23
|
} from "@opengeni/core";
|
|
28
24
|
import {
|
|
29
|
-
activatePreferenceRegistryRevision,
|
|
30
25
|
changePreferenceRegistryScope,
|
|
31
|
-
correctPreferenceRegistry,
|
|
32
|
-
createPreferenceRegistryProposal,
|
|
33
26
|
deactivatePreferenceRegistry,
|
|
34
27
|
getOrCreatePreferenceRegistrySnapshot,
|
|
35
28
|
getPreferenceRegistryDetail,
|
|
@@ -214,34 +207,20 @@ export function registerPreferenceRegistryRoutes(app: Hono, deps: ApiRouteDeps):
|
|
|
214
207
|
}
|
|
215
208
|
});
|
|
216
209
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
context,
|
|
221
|
-
deps,
|
|
222
|
-
workspaceId,
|
|
223
|
-
"workspace:read",
|
|
224
|
-
);
|
|
225
|
-
const { grant } = access;
|
|
226
|
-
const request = await parseBody(context, CreatePreferenceRegistryProposalRequest);
|
|
227
|
-
authorizePreferenceRegistryScopeMutation(access, request.scope);
|
|
228
|
-
try {
|
|
210
|
+
// Retain an explicit migration response, not a second single-text writer.
|
|
211
|
+
for (const path of ["/proposals", "/:preferenceId/activate", "/:preferenceId/correct"]) {
|
|
212
|
+
app.post(`${base}${path}`, async (context) => {
|
|
213
|
+
await requireAccessGrant(context, deps, context.req.param("workspaceId"), "workspace:read");
|
|
229
214
|
return context.json(
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
principalKind: grant.principalKind,
|
|
237
|
-
}),
|
|
238
|
-
),
|
|
239
|
-
201,
|
|
215
|
+
{
|
|
216
|
+
code: "SKILL_FILE_LIFECYCLE_REQUIRED",
|
|
217
|
+
message:
|
|
218
|
+
"Use the Skill content save, approve, or restore API. Skill metadata comes from SKILL.md frontmatter; this legacy endpoint makes no change.",
|
|
219
|
+
},
|
|
220
|
+
410,
|
|
240
221
|
);
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
}
|
|
244
|
-
});
|
|
222
|
+
});
|
|
223
|
+
}
|
|
245
224
|
|
|
246
225
|
app.get(`${base}/summary`, async (context) => {
|
|
247
226
|
const workspaceId = context.req.param("workspaceId");
|
|
@@ -300,66 +279,6 @@ export function registerPreferenceRegistryRoutes(app: Hono, deps: ApiRouteDeps):
|
|
|
300
279
|
}
|
|
301
280
|
});
|
|
302
281
|
|
|
303
|
-
app.post(`${base}/:preferenceId/activate`, async (context) => {
|
|
304
|
-
const workspaceId = context.req.param("workspaceId");
|
|
305
|
-
const access = await requireAccessGrantAuthorization(
|
|
306
|
-
context,
|
|
307
|
-
deps,
|
|
308
|
-
workspaceId,
|
|
309
|
-
"workspace:read",
|
|
310
|
-
);
|
|
311
|
-
const { grant } = access;
|
|
312
|
-
const id = preferenceId(context);
|
|
313
|
-
const request = await parseBody(context, ActivatePreferenceRegistryRevisionRequest);
|
|
314
|
-
try {
|
|
315
|
-
return context.json(
|
|
316
|
-
PreferenceRegistryMutationResponse.parse(
|
|
317
|
-
await activatePreferenceRegistryRevision(deps.db, {
|
|
318
|
-
accountId: grant.accountId,
|
|
319
|
-
workspaceId,
|
|
320
|
-
actorSubjectId: grant.subjectId,
|
|
321
|
-
principalKind: grant.principalKind,
|
|
322
|
-
preferenceId: id,
|
|
323
|
-
authorizeScope: (scope) => authorizePreferenceRegistryScopeMutation(access, scope),
|
|
324
|
-
...request,
|
|
325
|
-
}),
|
|
326
|
-
),
|
|
327
|
-
);
|
|
328
|
-
} catch (error) {
|
|
329
|
-
return preferenceError(context, error);
|
|
330
|
-
}
|
|
331
|
-
});
|
|
332
|
-
|
|
333
|
-
app.post(`${base}/:preferenceId/correct`, async (context) => {
|
|
334
|
-
const workspaceId = context.req.param("workspaceId");
|
|
335
|
-
const access = await requireAccessGrantAuthorization(
|
|
336
|
-
context,
|
|
337
|
-
deps,
|
|
338
|
-
workspaceId,
|
|
339
|
-
"workspace:read",
|
|
340
|
-
);
|
|
341
|
-
const { grant } = access;
|
|
342
|
-
const id = preferenceId(context);
|
|
343
|
-
const request = await parseBody(context, CorrectPreferenceRegistryRequest);
|
|
344
|
-
try {
|
|
345
|
-
return context.json(
|
|
346
|
-
PreferenceRegistryMutationResponse.parse(
|
|
347
|
-
await correctPreferenceRegistry(deps.db, {
|
|
348
|
-
...request,
|
|
349
|
-
accountId: grant.accountId,
|
|
350
|
-
workspaceId,
|
|
351
|
-
actorSubjectId: grant.subjectId,
|
|
352
|
-
principalKind: grant.principalKind,
|
|
353
|
-
preferenceId: id,
|
|
354
|
-
authorizeScope: (scope) => authorizePreferenceRegistryScopeMutation(access, scope),
|
|
355
|
-
}),
|
|
356
|
-
),
|
|
357
|
-
);
|
|
358
|
-
} catch (error) {
|
|
359
|
-
return preferenceError(context, error);
|
|
360
|
-
}
|
|
361
|
-
});
|
|
362
|
-
|
|
363
282
|
app.post(`${base}/:preferenceId/scope`, async (context) => {
|
|
364
283
|
const workspaceId = context.req.param("workspaceId");
|
|
365
284
|
const access = await requireAccessGrantAuthorization(
|
package/src/routes/sessions.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { getRetainedProviderCommand } from "@opengeni/db/retained-provider-commands";
|
|
1
2
|
import { scheduledSessionIds } from "@opengeni/db";
|
|
2
3
|
import { withSiteSessionOrigin } from "@opengeni/core";
|
|
3
4
|
import { resolveSiteSessionOrigin } from "../site-session-origin";
|
|
@@ -141,8 +142,7 @@ import {
|
|
|
141
142
|
projectSessionForRelatedAccess,
|
|
142
143
|
recordStreamAcknowledgment,
|
|
143
144
|
requestSessionCompaction,
|
|
144
|
-
|
|
145
|
-
withSessionCodexCapacityMutation,
|
|
145
|
+
switchSessionCodexAccount,
|
|
146
146
|
setSessionChannel,
|
|
147
147
|
updateSessionVariableSets,
|
|
148
148
|
ChannelNotFoundError,
|
|
@@ -181,6 +181,7 @@ import {
|
|
|
181
181
|
SessionTenancyInvalidRequestError,
|
|
182
182
|
SessionTenancyNotActivatedError,
|
|
183
183
|
HumanInputResponseValidationError,
|
|
184
|
+
SkillHumanResponseError,
|
|
184
185
|
latestWorkspaceCapture,
|
|
185
186
|
sessionLatestWorkspaceCapture,
|
|
186
187
|
renewSessionRealtimeInTransaction,
|
|
@@ -401,8 +402,18 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
401
402
|
message: "pty retained-process identity is stale; reopen the terminal",
|
|
402
403
|
});
|
|
403
404
|
}
|
|
405
|
+
const providerCommand = await getRetainedProviderCommand(db, {
|
|
406
|
+
accountId: ctx.accountId,
|
|
407
|
+
workspaceId: ctx.workspaceId,
|
|
408
|
+
sessionId: ctx.session.id,
|
|
409
|
+
processId: process.id,
|
|
410
|
+
});
|
|
404
411
|
handle.routingSession.adoptRetainedProcess({
|
|
405
|
-
process: {
|
|
412
|
+
process: {
|
|
413
|
+
id: process.id,
|
|
414
|
+
providerSessionId: process.providerSessionId,
|
|
415
|
+
...(providerCommand ? { providerCommand } : {}),
|
|
416
|
+
},
|
|
406
417
|
backend: {
|
|
407
418
|
sandboxId: null,
|
|
408
419
|
leaseEpoch: process.leaseEpoch,
|
|
@@ -1898,8 +1909,8 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
1898
1909
|
|
|
1899
1910
|
// Pin (or unpin) the session's Codex account. body { target: "auto" | "<id>" }:
|
|
1900
1911
|
// "auto" clears the pin (the session follows the workspace active pointer); a
|
|
1901
|
-
// uuid pins the session to that specific account.
|
|
1902
|
-
// turn
|
|
1912
|
+
// uuid pins the session to that specific account. Overrides a capacity-blocked
|
|
1913
|
+
// turn; a running attempt keeps its account. 404 when the session or the target
|
|
1903
1914
|
// account id isn't in the workspace.
|
|
1904
1915
|
app.post("/v1/workspaces/:workspaceId/sessions/:sessionId/codex-account", async (c) => {
|
|
1905
1916
|
const workspaceId = c.req.param("workspaceId");
|
|
@@ -1924,15 +1935,13 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
1924
1935
|
}
|
|
1925
1936
|
}
|
|
1926
1937
|
const pinned = target === "auto" ? null : target;
|
|
1927
|
-
const mutation = await
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
);
|
|
1935
|
-
const ok = mutation.result;
|
|
1938
|
+
const mutation = await switchSessionCodexAccount(db, {
|
|
1939
|
+
workspaceId,
|
|
1940
|
+
sessionId,
|
|
1941
|
+
credentialId: pinned,
|
|
1942
|
+
subjectId: grant.subjectId,
|
|
1943
|
+
});
|
|
1944
|
+
const ok = mutation.result.changed;
|
|
1936
1945
|
if (!ok) {
|
|
1937
1946
|
throw new HTTPException(404, {
|
|
1938
1947
|
message: "session or codex account not found",
|
|
@@ -1958,7 +1967,11 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
1958
1967
|
}),
|
|
1959
1968
|
),
|
|
1960
1969
|
);
|
|
1961
|
-
|
|
1970
|
+
await publishDurableSessionEvents(bus, workspaceId, sessionId, mutation.result.events);
|
|
1971
|
+
return c.json({
|
|
1972
|
+
pinned: target === "auto" ? "auto" : target,
|
|
1973
|
+
appliedTo: mutation.result.appliedTo,
|
|
1974
|
+
});
|
|
1962
1975
|
});
|
|
1963
1976
|
|
|
1964
1977
|
// Re-file the session into a workspace channel (rail organization only;
|
|
@@ -2740,14 +2753,16 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
2740
2753
|
!compact && mode === "forensic" && payloadMode === "full" && dbPage.fullPayloadsExact;
|
|
2741
2754
|
const page = boundSessionEventHttpPage(projected, {
|
|
2742
2755
|
direction,
|
|
2743
|
-
eventProjection:
|
|
2756
|
+
eventProjection: payloadMode === "full" ? "exact" : "bounded",
|
|
2744
2757
|
...(compactProjection
|
|
2745
2758
|
? { coveredThroughBySequence: compactProjection.coveredThroughBySequence }
|
|
2746
2759
|
: {}),
|
|
2747
2760
|
});
|
|
2748
2761
|
const hasMore = dbPage.hasMore || page.truncated;
|
|
2749
2762
|
c.header("X-OpenGeni-Page-Bytes", String(page.bytes));
|
|
2750
|
-
|
|
2763
|
+
// One oversized exact event is admitted alone; never advertise a maximum
|
|
2764
|
+
// smaller than the response we actually deliver.
|
|
2765
|
+
c.header("X-OpenGeni-Page-Max-Bytes", String(Math.max(1024 * 1024, page.bytes)));
|
|
2751
2766
|
c.header("X-OpenGeni-Page-Truncated", String(hasMore));
|
|
2752
2767
|
c.header("X-OpenGeni-Has-More", String(hasMore));
|
|
2753
2768
|
c.header("X-OpenGeni-Event-Mode", mode);
|
|
@@ -3187,14 +3202,31 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
3187
3202
|
requestId: event.payload.requestId,
|
|
3188
3203
|
response: event.payload.response,
|
|
3189
3204
|
respondedBy: grant.subjectId,
|
|
3205
|
+
canonicalHumanSession:
|
|
3206
|
+
authorization.canonicalManagedHumanSession || authorization.canonicalLocalHumanSession,
|
|
3190
3207
|
respondedByKind: childRequiresActionRespondedByKindForGrant(grant),
|
|
3191
3208
|
clientEventId: event.clientEventId ?? null,
|
|
3192
3209
|
});
|
|
3193
3210
|
} catch (error) {
|
|
3211
|
+
if (error instanceof SkillHumanResponseError) {
|
|
3212
|
+
throw new HTTPException(
|
|
3213
|
+
error.code === "conflict" ? 409 : error.code === "forbidden" ? 403 : 422,
|
|
3214
|
+
{
|
|
3215
|
+
message: error.message,
|
|
3216
|
+
},
|
|
3217
|
+
);
|
|
3218
|
+
}
|
|
3194
3219
|
if (error instanceof HumanInputResponseValidationError) {
|
|
3195
|
-
throw new HTTPException(
|
|
3196
|
-
|
|
3197
|
-
|
|
3220
|
+
throw new HTTPException(
|
|
3221
|
+
error.code === "HUMAN_AUTH_REQUIRED"
|
|
3222
|
+
? 403
|
|
3223
|
+
: error.code === "SKIP_NOT_ALLOWED"
|
|
3224
|
+
? 409
|
|
3225
|
+
: 422,
|
|
3226
|
+
{
|
|
3227
|
+
message: error.message,
|
|
3228
|
+
},
|
|
3229
|
+
);
|
|
3198
3230
|
}
|
|
3199
3231
|
throw error;
|
|
3200
3232
|
}
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { Context, Hono } from "hono";
|
|
3
|
+
import { HTTPException } from "hono/http-exception";
|
|
4
|
+
import { SkillFile, type SkillWriteReceipt } from "@opengeni/contracts";
|
|
5
|
+
import {
|
|
6
|
+
approveSkill,
|
|
7
|
+
listSkills,
|
|
8
|
+
readSkill,
|
|
9
|
+
requireAccessGrant,
|
|
10
|
+
requireAccessGrantAuthorization,
|
|
11
|
+
restoreSkill,
|
|
12
|
+
saveSkill,
|
|
13
|
+
validateSkillFiles,
|
|
14
|
+
type ApiRouteDeps,
|
|
15
|
+
} from "@opengeni/core";
|
|
16
|
+
import { applySkillFileChanges, buildPortableSkillArtifact } from "@opengeni/runtime/skill-library";
|
|
17
|
+
import { authorizePreferenceRegistryScopeMutation } from "./preference-registry";
|
|
18
|
+
|
|
19
|
+
const scopeSchema = z.enum(["workspace", "organization", "user"]);
|
|
20
|
+
const saveRequest = z
|
|
21
|
+
.object({
|
|
22
|
+
operationId: z.uuid(),
|
|
23
|
+
skillId: z.uuid(),
|
|
24
|
+
expectedRevisionId: z.uuid().nullable(),
|
|
25
|
+
expectedScopeVersion: z.number().int().nonnegative(),
|
|
26
|
+
scope: scopeSchema.default("workspace"),
|
|
27
|
+
stableKey: z.string().min(1).max(96),
|
|
28
|
+
files: z.array(SkillFile).max(128),
|
|
29
|
+
deletions: z.array(z.string().min(1).max(512)).max(128).default([]),
|
|
30
|
+
reason: z.string().min(1).max(2000),
|
|
31
|
+
})
|
|
32
|
+
.strict();
|
|
33
|
+
const revisionRequest = z
|
|
34
|
+
.object({
|
|
35
|
+
operationId: z.uuid(),
|
|
36
|
+
revisionId: z.uuid(),
|
|
37
|
+
expectedRevisionId: z.uuid().nullable(),
|
|
38
|
+
expectedScopeVersion: z.number().int().nonnegative(),
|
|
39
|
+
reason: z.string().min(1).max(2000),
|
|
40
|
+
})
|
|
41
|
+
.strict();
|
|
42
|
+
|
|
43
|
+
async function parseSkillRequest<S extends z.ZodType>(c: Context, schema: S): Promise<z.infer<S>> {
|
|
44
|
+
const parsed = schema.safeParse(await c.req.json().catch(() => null));
|
|
45
|
+
if (!parsed.success) throw new HTTPException(422, { message: "Invalid Skill request" });
|
|
46
|
+
return parsed.data;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function skillIdentifier(value: string): string {
|
|
50
|
+
const parsed = z.uuid().safeParse(value);
|
|
51
|
+
if (!parsed.success)
|
|
52
|
+
throw new HTTPException(422, { message: "Invalid Skill revision or identity" });
|
|
53
|
+
return parsed.data;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Human folder editing and the legacy Skill admin share scope authorization. */
|
|
57
|
+
export function registerSkillContentRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
58
|
+
const base = "/v1/workspaces/:workspaceId/skills/content";
|
|
59
|
+
app.get(base, async (c) => {
|
|
60
|
+
const workspaceId = c.req.param("workspaceId");
|
|
61
|
+
const grant = await requireAccessGrant(c, deps, workspaceId, "workspace:read");
|
|
62
|
+
const query = z
|
|
63
|
+
.object({
|
|
64
|
+
limit: z.coerce.number().int().min(1).max(250).default(100),
|
|
65
|
+
cursor: z.string().max(2048).optional(),
|
|
66
|
+
})
|
|
67
|
+
.safeParse(c.req.query());
|
|
68
|
+
if (!query.success) throw new HTTPException(422, { message: "Invalid Skill list query" });
|
|
69
|
+
let after: { stableKey: string; id: string } | undefined;
|
|
70
|
+
if (query.data.cursor !== undefined) {
|
|
71
|
+
try {
|
|
72
|
+
after = z
|
|
73
|
+
.object({ stableKey: z.string().min(1).max(128), id: z.uuid() })
|
|
74
|
+
.strict()
|
|
75
|
+
.parse(JSON.parse(Buffer.from(query.data.cursor, "base64url").toString("utf8")));
|
|
76
|
+
} catch {
|
|
77
|
+
throw new HTTPException(422, { message: "Invalid Skill list cursor" });
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
const rows = await listSkills(
|
|
81
|
+
deps.db,
|
|
82
|
+
{ accountId: grant.accountId, workspaceId, subjectId: grant.subjectId },
|
|
83
|
+
{ limit: query.data.limit + 1, metadataOnly: true, ...(after ? { after } : {}) },
|
|
84
|
+
);
|
|
85
|
+
const page = rows.slice(0, query.data.limit);
|
|
86
|
+
const last = page.at(-1);
|
|
87
|
+
return c.json({
|
|
88
|
+
skills: page.map(({ files: _files, ...summary }) => summary),
|
|
89
|
+
nextCursor:
|
|
90
|
+
rows.length > query.data.limit && last
|
|
91
|
+
? Buffer.from(JSON.stringify({ stableKey: last.stableKey, id: last.id })).toString(
|
|
92
|
+
"base64url",
|
|
93
|
+
)
|
|
94
|
+
: null,
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
app.get(`${base}/:skillId`, async (c) => {
|
|
98
|
+
const workspaceId = c.req.param("workspaceId");
|
|
99
|
+
const grant = await requireAccessGrant(c, deps, workspaceId, "workspace:read");
|
|
100
|
+
const skillId = skillIdentifier(c.req.param("skillId"));
|
|
101
|
+
const revision = c.req.query("revisionId");
|
|
102
|
+
const revisionId = revision === undefined ? undefined : skillIdentifier(revision);
|
|
103
|
+
const skill = await readSkill(
|
|
104
|
+
deps.db,
|
|
105
|
+
{ accountId: grant.accountId, workspaceId, subjectId: grant.subjectId },
|
|
106
|
+
skillId,
|
|
107
|
+
revisionId,
|
|
108
|
+
);
|
|
109
|
+
if (!skill) throw new HTTPException(404, { message: "Skill not found" });
|
|
110
|
+
return c.json(skill);
|
|
111
|
+
});
|
|
112
|
+
app.post(`${base}/save`, async (c) => {
|
|
113
|
+
const workspaceId = c.req.param("workspaceId");
|
|
114
|
+
const access = await requireAccessGrantAuthorization(c, deps, workspaceId, "workspace:read");
|
|
115
|
+
const request = await parseSkillRequest(c, saveRequest);
|
|
116
|
+
authorizePreferenceRegistryScopeMutation(access, request.scope);
|
|
117
|
+
const context = {
|
|
118
|
+
accountId: access.grant.accountId,
|
|
119
|
+
workspaceId,
|
|
120
|
+
subjectId: access.grant.subjectId,
|
|
121
|
+
};
|
|
122
|
+
const skillId = request.skillId;
|
|
123
|
+
const current = await readSkill(
|
|
124
|
+
deps.db,
|
|
125
|
+
context,
|
|
126
|
+
skillId,
|
|
127
|
+
request.expectedRevisionId ?? undefined,
|
|
128
|
+
);
|
|
129
|
+
if (request.expectedRevisionId && !current)
|
|
130
|
+
throw new HTTPException(404, { message: "Skill not found" });
|
|
131
|
+
if (current) {
|
|
132
|
+
authorizePreferenceRegistryScopeMutation(access, current.scope);
|
|
133
|
+
if (current.scope !== request.scope)
|
|
134
|
+
throw new HTTPException(409, {
|
|
135
|
+
message: "Move Skill scope through the existing scope-management workflow.",
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
let files;
|
|
139
|
+
try {
|
|
140
|
+
files = validateSkillFiles(
|
|
141
|
+
applySkillFileChanges(
|
|
142
|
+
request.expectedRevisionId ? (current?.files ?? []) : [],
|
|
143
|
+
request.files,
|
|
144
|
+
request.deletions,
|
|
145
|
+
),
|
|
146
|
+
);
|
|
147
|
+
buildPortableSkillArtifact(files);
|
|
148
|
+
} catch {
|
|
149
|
+
throw new HTTPException(400, {
|
|
150
|
+
message:
|
|
151
|
+
"Invalid Skill folder. SKILL.md requires valid YAML frontmatter with a name and description. Use unique relative text-file paths and respect the file and folder size limits.",
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
const receipt = await skillMutation(() =>
|
|
155
|
+
saveSkill(deps.db, {
|
|
156
|
+
accountId: context.accountId,
|
|
157
|
+
workspaceId,
|
|
158
|
+
actor: { kind: "human", principalKind: "human_session", subjectId: context.subjectId },
|
|
159
|
+
operationId: request.operationId,
|
|
160
|
+
skillId,
|
|
161
|
+
expectedRevisionId: request.expectedRevisionId,
|
|
162
|
+
expectedScopeVersion: request.expectedScopeVersion,
|
|
163
|
+
scope: request.scope,
|
|
164
|
+
stableKey: current?.stableKey ?? request.stableKey,
|
|
165
|
+
files,
|
|
166
|
+
reason: request.reason,
|
|
167
|
+
}),
|
|
168
|
+
);
|
|
169
|
+
return c.json(receipt);
|
|
170
|
+
});
|
|
171
|
+
for (const operation of ["approve", "restore"] as const) {
|
|
172
|
+
app.post(`${base}/:skillId/${operation}`, async (c) => {
|
|
173
|
+
const workspaceId = c.req.param("workspaceId");
|
|
174
|
+
const access = await requireAccessGrantAuthorization(c, deps, workspaceId, "workspace:read");
|
|
175
|
+
const skillId = skillIdentifier(c.req.param("skillId"));
|
|
176
|
+
const request = await parseSkillRequest(c, revisionRequest);
|
|
177
|
+
const current = await readSkill(
|
|
178
|
+
deps.db,
|
|
179
|
+
{ accountId: access.grant.accountId, workspaceId, subjectId: access.grant.subjectId },
|
|
180
|
+
skillId,
|
|
181
|
+
);
|
|
182
|
+
if (!current) throw new HTTPException(404, { message: "Skill not found" });
|
|
183
|
+
authorizePreferenceRegistryScopeMutation(access, current.scope);
|
|
184
|
+
const apply = operation === "approve" ? approveSkill : restoreSkill;
|
|
185
|
+
return c.json(
|
|
186
|
+
await skillMutation(() =>
|
|
187
|
+
apply(deps.db, {
|
|
188
|
+
...request,
|
|
189
|
+
skillId,
|
|
190
|
+
accountId: access.grant.accountId,
|
|
191
|
+
workspaceId,
|
|
192
|
+
actor: {
|
|
193
|
+
kind: "human",
|
|
194
|
+
principalKind: "human_session",
|
|
195
|
+
subjectId: access.grant.subjectId,
|
|
196
|
+
},
|
|
197
|
+
}),
|
|
198
|
+
),
|
|
199
|
+
);
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
async function skillMutation(run: () => Promise<SkillWriteReceipt>): Promise<SkillWriteReceipt> {
|
|
205
|
+
try {
|
|
206
|
+
return await run();
|
|
207
|
+
} catch (error) {
|
|
208
|
+
let current: unknown = error;
|
|
209
|
+
for (let depth = 0; depth < 4 && current && typeof current === "object"; depth++) {
|
|
210
|
+
const candidate = current as { code?: unknown; cause?: unknown };
|
|
211
|
+
if (candidate.code === "23505" || candidate.code === "40001")
|
|
212
|
+
throw new HTTPException(409, {
|
|
213
|
+
message:
|
|
214
|
+
"Skill changed or this operation key was reused. Reload and reconcile the change.",
|
|
215
|
+
});
|
|
216
|
+
if (candidate.code === "42501")
|
|
217
|
+
throw new HTTPException(403, {
|
|
218
|
+
message: "Skill change is not permitted for this actor or scope.",
|
|
219
|
+
});
|
|
220
|
+
if (candidate.code === "22023" || candidate.code === "23514")
|
|
221
|
+
throw new HTTPException(400, { message: "Skill content or revision request is invalid." });
|
|
222
|
+
current = candidate.cause;
|
|
223
|
+
}
|
|
224
|
+
throw error;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { SkillActor, type SkillActor as SkillActorType } from "@opengeni/contracts";
|
|
2
|
+
import {
|
|
3
|
+
requireResolvedAccessGrantAuthorization,
|
|
4
|
+
type AccessGrantAuthorization,
|
|
5
|
+
} from "@opengeni/core";
|
|
6
|
+
import { HTTPException } from "hono/http-exception";
|
|
7
|
+
|
|
8
|
+
/** Use authenticated request provenance, never a caller-supplied actor or subject alone. */
|
|
9
|
+
export function skillInstallerActor(access: AccessGrantAuthorization): SkillActorType {
|
|
10
|
+
const grant = requireResolvedAccessGrantAuthorization(access, access.grant.workspaceId);
|
|
11
|
+
const metadata = grant.metadata ?? {};
|
|
12
|
+
const hasAttempt = ["sessionId", "turnId", "attemptId", "executionGeneration"].some(
|
|
13
|
+
(key) => metadata[key] !== undefined,
|
|
14
|
+
);
|
|
15
|
+
if (hasAttempt || grant.principalKind === "agent_attempt") {
|
|
16
|
+
const parsed = SkillActor.safeParse({
|
|
17
|
+
kind: "agent",
|
|
18
|
+
sessionId: metadata.sessionId,
|
|
19
|
+
turnId: metadata.turnId,
|
|
20
|
+
attemptId: metadata.attemptId,
|
|
21
|
+
executionGeneration: metadata.executionGeneration,
|
|
22
|
+
});
|
|
23
|
+
if (!parsed.success)
|
|
24
|
+
throw new HTTPException(403, {
|
|
25
|
+
message: "Skill installation requires complete signed attempt authority.",
|
|
26
|
+
});
|
|
27
|
+
return parsed.data;
|
|
28
|
+
}
|
|
29
|
+
if (
|
|
30
|
+
grant.principalKind === "human_session" &&
|
|
31
|
+
!grant.serviceInitiator &&
|
|
32
|
+
!grant.serviceInitiatorContext &&
|
|
33
|
+
!grant.subjectId.startsWith("api_key:")
|
|
34
|
+
) {
|
|
35
|
+
return { kind: "human", principalKind: "human_session", subjectId: grant.subjectId };
|
|
36
|
+
}
|
|
37
|
+
if (
|
|
38
|
+
grant.principalKind === "service" ||
|
|
39
|
+
grant.principalKind === "api_key" ||
|
|
40
|
+
grant.principalKind === "configured_key"
|
|
41
|
+
) {
|
|
42
|
+
return { kind: "service", principalKind: grant.principalKind, subjectId: grant.subjectId };
|
|
43
|
+
}
|
|
44
|
+
throw new HTTPException(403, {
|
|
45
|
+
message:
|
|
46
|
+
"Skill installation requires authenticated human, service, or complete live-attempt authority.",
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Non-Skill component removals remain available to their existing callers. */
|
|
51
|
+
export function skillRemovalActor(access: AccessGrantAuthorization): SkillActorType | undefined {
|
|
52
|
+
const grant = requireResolvedAccessGrantAuthorization(access, access.grant.workspaceId);
|
|
53
|
+
if (
|
|
54
|
+
grant.principalKind !== "human_session" ||
|
|
55
|
+
grant.serviceInitiator ||
|
|
56
|
+
grant.serviceInitiatorContext ||
|
|
57
|
+
grant.subjectId.startsWith("api_key:") ||
|
|
58
|
+
["sessionId", "turnId", "attemptId", "executionGeneration"].some(
|
|
59
|
+
(key) => grant.metadata?.[key] !== undefined,
|
|
60
|
+
)
|
|
61
|
+
)
|
|
62
|
+
return undefined;
|
|
63
|
+
return skillInstallerActor(access);
|
|
64
|
+
}
|