@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.
Files changed (38) hide show
  1. package/dist/app.js +1 -1
  2. package/dist/{chunk-EAYCECQK.js → chunk-5X3XM6CE.js} +1841 -1528
  3. package/dist/chunk-5X3XM6CE.js.map +1 -0
  4. package/dist/http/sse.d.ts +4 -3
  5. package/dist/index.js +5 -3
  6. package/dist/index.js.map +1 -1
  7. package/dist/integrations/github-skill-source.d.ts +1 -5
  8. package/dist/mcp/remember.d.ts +1 -1
  9. package/dist/mcp/scheduled-task-view.d.ts +3 -3
  10. package/dist/mcp/server.d.ts +4 -4
  11. package/dist/routes/skill-content.d.ts +4 -0
  12. package/dist/routes/skill-install-authority.d.ts +6 -0
  13. package/dist/transcription/providers/xai-subscription.d.ts +1 -0
  14. package/dist/transcription/service.d.ts +2 -1
  15. package/package.json +19 -19
  16. package/src/app.ts +18 -0
  17. package/src/http/sse.ts +9 -25
  18. package/src/index.ts +3 -1
  19. package/src/integrations/github-skill-source.ts +2 -142
  20. package/src/mcp/company-brain-governed-writes.ts +15 -21
  21. package/src/mcp/remember.ts +14 -15
  22. package/src/mcp/server.ts +7 -4
  23. package/src/routes/automations.ts +35 -0
  24. package/src/routes/packs.ts +61 -8
  25. package/src/routes/plugins.ts +48 -5
  26. package/src/routes/preference-registry.ts +12 -93
  27. package/src/routes/sessions.ts +52 -20
  28. package/src/routes/skill-content.ts +226 -0
  29. package/src/routes/skill-install-authority.ts +64 -0
  30. package/src/routes/skills.ts +38 -3
  31. package/src/routes/transcription-recordings.ts +30 -1
  32. package/src/routes/transcriptions.ts +4 -0
  33. package/src/transcription/providers/codex-subscription.ts +2 -0
  34. package/src/transcription/providers/openai.ts +1 -0
  35. package/src/transcription/providers/xai-subscription.ts +49 -3
  36. package/src/transcription/service.ts +43 -6
  37. package/src/xai-subscription-auth.ts +9 -2
  38. 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
- app.post(`${base}/proposals`, async (context) => {
218
- const workspaceId = context.req.param("workspaceId");
219
- const access = await requireAccessGrantAuthorization(
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
- PreferenceRegistryRecord.parse(
231
- await createPreferenceRegistryProposal(deps.db, {
232
- ...request,
233
- accountId: grant.accountId,
234
- workspaceId,
235
- actorSubjectId: grant.subjectId,
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
- } catch (error) {
242
- return preferenceError(context, error);
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(
@@ -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
- setSessionCodexPinInTransaction,
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: { id: process.id, providerSessionId: process.providerSessionId },
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. The pin applies to the NEXT
1902
- // turn (the worker reads it at turn start). 404 when the session or the target
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 withSessionCodexCapacityMutation(
1928
- db,
1929
- { workspaceId, reason: "codex_manual_session_pin_changed" },
1930
- async (tx) => {
1931
- const changed = await setSessionCodexPinInTransaction(tx, workspaceId, sessionId, pinned);
1932
- return { result: changed, changed };
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
- return c.json({ pinned: target === "auto" ? "auto" : target });
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: forensicExact ? "exact" : "bounded",
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
- c.header("X-OpenGeni-Page-Max-Bytes", String(1024 * 1024));
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(error.code === "SKIP_NOT_ALLOWED" ? 409 : 422, {
3196
- message: error.message,
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
+ }