@opengeni/api-router 4.0.0 → 4.1.0-canary.35855175602001

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/src/mcp/server.ts CHANGED
@@ -9,6 +9,7 @@ import {
9
9
  setSessionChannel,
10
10
  } from "@opengeni/db";
11
11
  import { createHash, randomUUID } from "node:crypto";
12
+ import { mintEnrollToken } from "../sandbox/enrollment";
12
13
  import { capabilityAccountReadiness } from "./capability-account-readiness";
13
14
  import {
14
15
  prepareWorkspaceArtifactUpload,
@@ -280,6 +281,8 @@ import {
280
281
  boundRigDetailMcp,
281
282
  SESSION_EVENT_MCP_MAX_BYTES,
282
283
  } from "./session-view";
284
+ import { completeChildReadSequences } from "./child-read-evidence";
285
+ import { acknowledgeConsumedChildEvents } from "@opengeni/db";
283
286
  import {
284
287
  SESSION_WAIT_COMPLETION_EVENT_TYPES,
285
288
  SESSION_WAIT_DEFAULT_SECONDS,
@@ -1709,7 +1712,7 @@ function registerSandboxFileArtifactTool(
1709
1712
  "sandbox_file_publish",
1710
1713
  {
1711
1714
  description:
1712
- "Publish one exact file from this session's /workspace into durable workspace storage. Use this before presenting a ZIP, CSV, JSON, Markdown, HTML, PDF, Office file, or other sandbox output as downloadable. Returns a permanent, authenticated artifact receipt; never invent or expose a sandbox: URL as the durable result.",
1715
+ "Publish one exact file from this session's /workspace into durable workspace storage. Use this before presenting a video, ZIP, CSV, JSON, Markdown, HTML, PDF, Office file, or other sandbox output as downloadable. Present the returned artifact.artifactId as [Open file](artifact:<artifactId>), or ![Preview](artifact:<artifactId>) for inline image/video/audio/PDF previews. Replace <artifactId> with the exact returned ID and use a descriptive label. Never expose a sandbox: URL as the durable result.",
1713
1716
  inputSchema: {
1714
1717
  path: z4.string().min(1).max(4_096),
1715
1718
  },
@@ -3780,7 +3783,7 @@ function registerFleetTools(
3780
3783
  "sandbox_provision",
3781
3784
  {
3782
3785
  description:
3783
- "Provision a new sandbox for the fleet. kind=selfhosted returns device-flow enrollment instructions to share with a HUMAN (install the agent + enroll their machine with loud whole-machine consent — the agent cannot self-consent). kind=modal creates a named Modal sandbox record, but it is NOT yet attachable as a swap target: routing a session onto a second Modal box is not supported yet, so sandbox_swap to its id is rejected. Use the session's own box (the default) or attach a Connected Machine instead.",
3786
+ "Provision a new sandbox for the fleet. kind=selfhosted returns interactive device-flow instructions for a human to approve. For authorized headless enrollment, use connected_machine_enroll_token when available instead. kind=modal creates a named Modal sandbox record, but it is NOT yet attachable as a swap target: routing a session onto a second Modal box is not supported yet, so sandbox_swap to its id is rejected. Use the session's own box (the default) or attach a Connected Machine instead.",
3784
3787
  inputSchema: {
3785
3788
  kind: z4.enum(["selfhosted", "modal"]),
3786
3789
  name: z4.string().min(1).max(120).optional(),
@@ -3796,11 +3799,11 @@ function registerFleetTools(
3796
3799
  );
3797
3800
  }
3798
3801
 
3799
- // Workspace-admin/operator surface for removing a connected machine. This is
3802
+ // Enrollment-management surface for creating tokens and removing machines. This is
3800
3803
  // deliberately separate from the session-scoped fleet tools: a worker can list,
3801
3804
  // attach, or run on a machine only with session authority, while removal requires
3802
- // the explicit high-trust enrollments:manage permission and never accepts a
3803
- // sandbox id. A Modal record therefore cannot be removed through this operation.
3805
+ // the explicit enrollments:manage permission. Removal accepts enrollment ids,
3806
+ // never sandbox ids; a Modal record cannot be removed through this operation.
3804
3807
  function registerConnectedMachineTools(
3805
3808
  server: McpServer,
3806
3809
  deps: ApiRouteDeps,
@@ -3808,11 +3811,40 @@ function registerConnectedMachineTools(
3808
3811
  sessionId: string | null,
3809
3812
  json: JsonResult,
3810
3813
  ): void {
3814
+ server.registerTool(
3815
+ "connected_machine_enroll_token",
3816
+ {
3817
+ description:
3818
+ "Create a short-lived Connected Machine enrollment token for this workspace using existing enrollments:manage authority. Returns the token, expiry and Unix/PowerShell install commands. Run the appropriate command on the intended machine through an already-authorized execution path, then verify it with sandboxes_list. No separate device approval is required. Screen control is optional and defaults off. The token grants whole-machine access on enrollment; do not put it in public code or unrelated logs. Existing machine connections are preserved.",
3819
+ inputSchema: { allowScreenControl: z4.boolean().optional() },
3820
+ },
3821
+ async ({ allowScreenControl }) => {
3822
+ if (sessionId) {
3823
+ await authorizeFirstPartySession(deps, grant, sessionId, "session.first_party_mcp.call");
3824
+ }
3825
+ const minted = await mintEnrollToken(deps, {
3826
+ accountId: grant.accountId,
3827
+ workspaceId: grant.workspaceId,
3828
+ allowScreenControl: allowScreenControl ?? false,
3829
+ });
3830
+ if (!minted) throw new Error("enrollment credential plane is not configured");
3831
+ const base = (deps.settings.publicBaseUrl ?? "https://app.opengeni.ai").replace(/\/+$/, "");
3832
+ const unixQuote = (value: string) => `'${value.replaceAll("'", "'\\''")}'`;
3833
+ const windowsQuote = (value: string) => `'${value.replaceAll("'", "''")}'`;
3834
+ return json({
3835
+ ...minted,
3836
+ workspaceId: grant.workspaceId,
3837
+ allowScreenControl: allowScreenControl ?? false,
3838
+ installCommandUnix: `curl -fsSL ${unixQuote(`${base}/install.sh`)} | OPENGENI_API_URL=${unixQuote(base)} OPENGENI_ENROLL_TOKEN=${unixQuote(minted.token)} sh`,
3839
+ installCommandWindows: `$env:OPENGENI_API_URL=${windowsQuote(base)}; $env:OPENGENI_ENROLL_TOKEN=${windowsQuote(minted.token)}; irm ${windowsQuote(`${base}/install.ps1`)} | iex`,
3840
+ });
3841
+ },
3842
+ );
3811
3843
  server.registerTool(
3812
3844
  "connected_machine_remove",
3813
3845
  {
3814
3846
  description:
3815
- "Remove one enrolled self-hosted machine while it is offline. Access is revoked, future heartbeat/reconnect credentials are rejected, and session, route, lease, archive, and audit history is retained. Idle dependent sessions are detached atomically; machine-home sessions become compute-less (backend none) until another sandbox is selected. Active turns, live leases, and recovery work remain fail-closed blockers whose typed outcome explains what must settle before retrying. Pass the enrollmentId from the Machines surface, never a Modal sandbox id. Reconnecting later requires a fresh human-approved device-flow enrollment.",
3847
+ "Remove one enrolled self-hosted machine while it is offline. Access is revoked, future heartbeat/reconnect credentials are rejected, and session, route, lease, archive, and audit history is retained. Idle dependent sessions are detached atomically; machine-home sessions become compute-less (backend none) until another sandbox is selected. Active turns, live leases, and recovery work remain fail-closed blockers whose typed outcome explains what must settle before retrying. Pass the enrollmentId from the Machines surface, never a Modal sandbox id. Reconnecting later requires fresh enrollment through device approval or an authorized enrollment token.",
3816
3848
  inputSchema: {
3817
3849
  enrollmentId: z4.string().uuid(),
3818
3850
  expectedUpdatedAt: z4.string().datetime({ offset: true }).optional(),
@@ -4221,6 +4253,24 @@ function registerWorkspaceOrchestrationTools(
4221
4253
  sessionCreateVisible: boolean,
4222
4254
  json: JsonResult,
4223
4255
  ): void {
4256
+ const acknowledgeReads = async (children: { sessionId: string; sequences: number[] }[]) => {
4257
+ if (
4258
+ !callerSessionId ||
4259
+ !exactAgentAttemptClaims(grant) ||
4260
+ !children.some((child) => child.sequences.length > 0)
4261
+ )
4262
+ return;
4263
+ // Never use grant.subjectId, session creator, or the latest queue human.
4264
+ // Reauthorize after the bounded read, including after a blocking wait.
4265
+ const actor = await requireLiveAgentAttemptAuthorization(deps.db, grant, callerSessionId);
4266
+ if (!actor.initiatingHumanSubjectId) return;
4267
+ await acknowledgeConsumedChildEvents(deps.db, {
4268
+ workspaceId: grant.workspaceId,
4269
+ sessionId: callerSessionId,
4270
+ subjectId: actor.initiatingHumanSubjectId,
4271
+ children,
4272
+ });
4273
+ };
4224
4274
  if (can("sessions:read")) {
4225
4275
  server.registerTool(
4226
4276
  "sessions_list",
@@ -4572,31 +4622,31 @@ function registerWorkspaceOrchestrationTools(
4572
4622
  if (view !== "debug" && !auditRequested) {
4573
4623
  const { readSessionEventView } = await import("./session-event-view");
4574
4624
  const { listSessionEventSlices } = await import("@opengeni/db/session-event-slices");
4575
- return json(
4576
- await readSessionEventView(
4577
- {
4625
+ const page = await readSessionEventView(
4626
+ {
4627
+ sessionId,
4628
+ view,
4629
+ cursor,
4630
+ callId,
4631
+ includeArguments,
4632
+ includeOutput,
4633
+ after,
4634
+ before,
4635
+ direction: requestedDirection,
4636
+ limit,
4637
+ },
4638
+ (options) =>
4639
+ listSessionEventSlices(
4640
+ deps.db,
4641
+ grant.workspaceId,
4578
4642
  sessionId,
4579
- view,
4580
- cursor,
4581
- callId,
4582
- includeArguments,
4583
- includeOutput,
4584
- after,
4585
- before,
4586
- direction: requestedDirection,
4587
- limit,
4588
- },
4589
- (options) =>
4590
- listSessionEventSlices(
4591
- deps.db,
4592
- grant.workspaceId,
4593
- sessionId,
4594
- options,
4595
- (legacyOptions) =>
4596
- listSessionEventPage(deps.db, grant.workspaceId, sessionId, legacyOptions),
4597
- ),
4598
- ),
4643
+ options,
4644
+ (legacyOptions) =>
4645
+ listSessionEventPage(deps.db, grant.workspaceId, sessionId, legacyOptions),
4646
+ ),
4599
4647
  );
4648
+ await acknowledgeReads([{ sessionId, sequences: completeChildReadSequences(page) }]);
4649
+ return json(page);
4600
4650
  }
4601
4651
  if (
4602
4652
  cursor !== undefined ||
@@ -4640,33 +4690,48 @@ function registerWorkspaceOrchestrationTools(
4640
4690
  });
4641
4691
  if (requestedResultMode === "compact") {
4642
4692
  const event = dbPage.events[0];
4643
- return json(
4644
- event
4645
- ? boundSessionEventCompactResult(
4646
- compactSessionEventResult(
4647
- event,
4648
- latestClass!,
4649
- dbPage.coveredSequence ?? {
4650
- first: event.sequence,
4651
- last: event.sequence,
4652
- },
4653
- ),
4654
- )
4655
- : null,
4656
- );
4693
+ const result = event
4694
+ ? boundSessionEventCompactResult(
4695
+ compactSessionEventResult(
4696
+ event,
4697
+ latestClass!,
4698
+ dbPage.coveredSequence ?? {
4699
+ first: event.sequence,
4700
+ last: event.sequence,
4701
+ },
4702
+ ),
4703
+ )
4704
+ : null;
4705
+ if (
4706
+ result &&
4707
+ ["turn.completed", "agent.message.completed"].includes(result.type) &&
4708
+ !result.truncation.truncated &&
4709
+ dbPage.fullPayloadsExact
4710
+ ) {
4711
+ await acknowledgeReads([{ sessionId, sequences: [result.sequence] }]);
4712
+ }
4713
+ return json(result);
4657
4714
  }
4658
- return json(
4659
- boundSessionEventMcpPage({
4660
- events: dbPage.events,
4661
- mode,
4662
- payloadMode,
4663
- direction,
4664
- sourceHasMore: dbPage.hasMore,
4665
- sourceTruncatedBy: dbPage.truncatedBy,
4666
- after: after ?? 0,
4667
- before: before ?? null,
4668
- }),
4669
- );
4715
+ const page = boundSessionEventMcpPage({
4716
+ events: dbPage.events,
4717
+ mode,
4718
+ payloadMode,
4719
+ direction,
4720
+ sourceHasMore: dbPage.hasMore,
4721
+ sourceTruncatedBy: dbPage.truncatedBy,
4722
+ after: after ?? 0,
4723
+ before: before ?? null,
4724
+ });
4725
+ if (
4726
+ payloadMode === "full" &&
4727
+ dbPage.fullPayloadsExact &&
4728
+ !page.truncation?.reasons.includes("model_payload")
4729
+ ) {
4730
+ await acknowledgeReads([
4731
+ { sessionId, sequences: page.events.map((event) => event.sequence) },
4732
+ ]);
4733
+ }
4734
+ return json(page);
4670
4735
  },
4671
4736
  );
4672
4737
 
@@ -4721,56 +4786,65 @@ function registerWorkspaceOrchestrationTools(
4721
4786
  // (tests) may pass no extra at all.
4722
4787
  const signal: AbortSignal | undefined = extra?.signal;
4723
4788
  const workspaceId = grant.workspaceId;
4789
+ const incompleteWaitEvents = new Set<string>();
4724
4790
  // A NATS subscription is live fanout only; the durable session_events
4725
4791
  // read below is the authority. Bun.serve idleTimeout (255 s) and the
4726
4792
  // 60 s MCP client request timeout both exceed the 50 s cap.
4727
- return json(
4728
- await waitForSessionChanges({
4729
- targets,
4730
- ownSessionId,
4731
- maxWaitMs: (maxWaitSeconds ?? SESSION_WAIT_DEFAULT_SECONDS) * 1_000,
4732
- targetEventTypes,
4733
- targetEventMatches:
4734
- waitFor === "completion" ? sessionWaitCompletionEventMatches : undefined,
4735
- signal,
4736
- source: {
4737
- reauthorizeTargets: async (sessionIds) => {
4738
- for (const targetSessionId of sessionIds) {
4739
- await authorizeFirstPartySession(
4740
- deps,
4741
- grant,
4742
- targetSessionId,
4743
- "session.events.read",
4744
- );
4745
- }
4746
- },
4747
- readTargetEvents: async (target) => {
4748
- const page = await listSessionEventPage(deps.db, workspaceId, target.sessionId, {
4749
- after: target.afterSequence,
4750
- direction: "after",
4751
- limit: SESSION_WAIT_EVENTS_PER_TARGET,
4752
- payloadMode: "full",
4753
- includeTypes: targetEventTypes,
4754
- maxBytes: SESSION_EVENT_MCP_MAX_BYTES * 4,
4755
- });
4756
- return { events: page.events, hasMore: page.hasMore };
4757
- },
4758
- readOwnPendingUpdateKinds:
4759
- ownSessionId === null
4760
- ? null
4761
- : async () =>
4762
- (
4763
- await listOutstandingSessionSystemUpdates(
4764
- deps.db,
4765
- workspaceId,
4766
- ownSessionId,
4767
- )
4768
- ).map((update) => update.kind),
4769
- subscribe: (targetSessionId, onEvents) =>
4770
- deps.bus.subscribe(workspaceId, targetSessionId, onEvents),
4793
+ const result = await waitForSessionChanges({
4794
+ targets,
4795
+ ownSessionId,
4796
+ maxWaitMs: (maxWaitSeconds ?? SESSION_WAIT_DEFAULT_SECONDS) * 1_000,
4797
+ targetEventTypes,
4798
+ targetEventMatches:
4799
+ waitFor === "completion" ? sessionWaitCompletionEventMatches : undefined,
4800
+ signal,
4801
+ source: {
4802
+ reauthorizeTargets: async (sessionIds) => {
4803
+ for (const targetSessionId of sessionIds) {
4804
+ await authorizeFirstPartySession(
4805
+ deps,
4806
+ grant,
4807
+ targetSessionId,
4808
+ "session.events.read",
4809
+ );
4810
+ }
4771
4811
  },
4772
- }),
4773
- );
4812
+ readTargetEvents: async (target) => {
4813
+ const page = await listSessionEventPage(deps.db, workspaceId, target.sessionId, {
4814
+ after: target.afterSequence,
4815
+ direction: "after",
4816
+ limit: SESSION_WAIT_EVENTS_PER_TARGET,
4817
+ payloadMode: "full",
4818
+ includeTypes: targetEventTypes,
4819
+ maxBytes: SESSION_EVENT_MCP_MAX_BYTES * 4,
4820
+ });
4821
+ if (!page.fullPayloadsExact) {
4822
+ for (const event of page.events) incompleteWaitEvents.add(event.id);
4823
+ }
4824
+ return { events: page.events, hasMore: page.hasMore };
4825
+ },
4826
+ readOwnPendingUpdateKinds:
4827
+ ownSessionId === null
4828
+ ? null
4829
+ : async () =>
4830
+ (
4831
+ await listOutstandingSessionSystemUpdates(deps.db, workspaceId, ownSessionId)
4832
+ ).map((update) => update.kind),
4833
+ subscribe: (targetSessionId, onEvents) =>
4834
+ deps.bus.subscribe(workspaceId, targetSessionId, onEvents),
4835
+ },
4836
+ });
4837
+ if (!result.aborted && !result.truncated) {
4838
+ await acknowledgeReads(
4839
+ result.changed.map((target) => ({
4840
+ sessionId: target.sessionId,
4841
+ sequences: target.events
4842
+ .filter((event) => event.contentComplete && !incompleteWaitEvents.has(event.id))
4843
+ .map((event) => event.sequence),
4844
+ })),
4845
+ );
4846
+ }
4847
+ return json(result);
4774
4848
  },
4775
4849
  );
4776
4850
 
@@ -164,6 +164,8 @@ export function sessionWaitSemanticClass(type: SessionEventType): SessionEventSe
164
164
  export type SessionWaitTarget = { sessionId: string; afterSequence: number };
165
165
 
166
166
  export type SessionWaitEventSummary = {
167
+ /** True only when every semantic result field survived all projections. */
168
+ contentComplete?: boolean;
167
169
  id: string;
168
170
  sequence: number;
169
171
  type: SessionEventType;
@@ -521,6 +523,20 @@ export function summarizeSessionWaitEvent(
521
523
  } else if (typeof compact.result === "string" && compact.result !== compact.text) {
522
524
  summary.result = clampSummaryString(compact.result, textChars);
523
525
  }
526
+ // Other wait summaries may omit actionable fields (e.g. human-input
527
+ // questions); absence of a truncation marker is not proof of full content.
528
+ summary.contentComplete =
529
+ ["turn.completed", "agent.message.completed"].includes(event.type) &&
530
+ !compact.truncation.truncated &&
531
+ text === compact.text &&
532
+ JSON.stringify(failure) === JSON.stringify(compact.failure) &&
533
+ (compact.output === null ||
534
+ compact.output === compact.text ||
535
+ JSON.stringify(compact.output) === JSON.stringify(summary.result)) &&
536
+ (compact.result === null ||
537
+ compact.result === undefined ||
538
+ compact.result === compact.text ||
539
+ JSON.stringify(summary.result) === JSON.stringify(compact.result));
524
540
  return summary;
525
541
  }
526
542
 
@@ -570,6 +586,7 @@ export function boundSessionWaitResult(
570
586
  ...target,
571
587
  events: target.events.map((event) => ({
572
588
  ...event,
589
+ contentComplete: false,
573
590
  text: event.text === null ? null : clampSummaryString(event.text, textChars),
574
591
  ...(typeof event.result === "string"
575
592
  ? { result: clampSummaryString(event.result, textChars) }
@@ -7,14 +7,18 @@
7
7
  // is added to isAuthExempt. Secrets never leave the server: status/usage read the
8
8
  // decrypted token only to call the codex backend; the token is never returned.
9
9
 
10
- import { environmentsEncryptionKeyBytes, productLabelForModelId } from "@opengeni/config";
10
+ import {
11
+ environmentsEncryptionKeyBytes,
12
+ configuredModels,
13
+ getSettings,
14
+ withCodexCatalogProvider,
15
+ type Settings,
16
+ } from "@opengeni/config";
11
17
  import {
12
18
  accessTokenExpiry,
13
19
  buildCodexUsageWindowFromCache,
14
20
  CODEX_CLIENT_VERSION,
15
- CODEX_FALLBACK_MODEL_SLUGS,
16
21
  CODEX_FIVE_HOUR_WINDOW_SECONDS,
17
- CODEX_MODEL_ID_PREFIX,
18
22
  CODEX_PROVIDER_ID,
19
23
  CODEX_WEEKLY_WINDOW_SECONDS,
20
24
  CodexDeviceError,
@@ -193,20 +197,22 @@ function codexUsageJson(payload: CodexUsagePayload): {
193
197
  return { status: payload.status, usage: payload };
194
198
  }
195
199
 
196
- export function codexModelsForPicker(): Array<{
200
+ export function codexModelsForPicker(settings: Settings = getSettings()): Array<{
197
201
  id: string;
198
202
  label: string;
199
203
  provider: string;
200
204
  providerLabel: string;
201
205
  api: "responses";
202
206
  }> {
203
- return CODEX_FALLBACK_MODEL_SLUGS.map((slug) => ({
204
- id: `${CODEX_MODEL_ID_PREFIX}${slug}`,
205
- label: productLabelForModelId(slug),
206
- provider: CODEX_PROVIDER_ID,
207
- providerLabel: CODEX_PROVIDER_LABEL,
208
- api: "responses" as const,
209
- }));
207
+ return configuredModels(withCodexCatalogProvider(settings))
208
+ .filter((model) => model.providerId === CODEX_PROVIDER_ID)
209
+ .map((model) => ({
210
+ id: model.id,
211
+ label: model.label,
212
+ provider: CODEX_PROVIDER_ID,
213
+ providerLabel: CODEX_PROVIDER_LABEL,
214
+ api: "responses" as const,
215
+ }));
210
216
  }
211
217
  import { createSignedState, readSignedState } from "@opengeni/github";
212
218
  import {
@@ -214,6 +220,7 @@ import {
214
220
  hasPermission,
215
221
  requireAccessGrant,
216
222
  requireCanonicalLocalAccountAdministrator,
223
+ resolveCatalogSettings,
217
224
  type ApiRouteDeps,
218
225
  } from "@opengeni/core";
219
226
  import type { Context, Hono } from "hono";
@@ -1284,7 +1291,7 @@ export function registerCodexRoutes(app: Hono, deps: ApiRouteDeps): void {
1284
1291
  now: new Date(),
1285
1292
  });
1286
1293
  let valid = false;
1287
- const models = codexModelsForPicker();
1294
+ const models = codexModelsForPicker((await resolveCatalogSettings(db, settings)).settings);
1288
1295
  let catalogError: string | null = null;
1289
1296
  try {
1290
1297
  const cred = status?.credentialId
@@ -1,8 +1,8 @@
1
1
  import { z } from "zod";
2
2
  import type { Context, Hono } from "hono";
3
3
  import { HTTPException } from "hono/http-exception";
4
- import { SkillFile, type SkillWriteReceipt } from "@opengeni/contracts";
5
- import { readSkillRemovalScope } from "@opengeni/db";
4
+ import { SkillFile, SKILL_MAX_FILES, type SkillWriteReceipt } from "@opengeni/contracts";
5
+ import { getSessionForSubject, readSkillRemovalScope } from "@opengeni/db";
6
6
  import {
7
7
  approveSkill,
8
8
  rejectSkill,
@@ -27,8 +27,8 @@ const saveRequest = z
27
27
  expectedScopeVersion: z.number().int().nonnegative(),
28
28
  scope: scopeSchema.default("workspace"),
29
29
  stableKey: z.string().min(1).max(96),
30
- files: z.array(SkillFile).max(128),
31
- deletions: z.array(z.string().min(1).max(512)).max(128).default([]),
30
+ files: z.array(SkillFile).max(SKILL_MAX_FILES),
31
+ deletions: z.array(z.string().min(1).max(512)).max(SKILL_MAX_FILES).default([]),
32
32
  reason: z.string().min(1).max(2000),
33
33
  })
34
34
  .strict();
@@ -66,9 +66,20 @@ export function registerSkillContentRoutes(app: Hono, deps: ApiRouteDeps): void
66
66
  .object({
67
67
  limit: z.coerce.number().int().min(1).max(250).default(100),
68
68
  cursor: z.string().max(2048).optional(),
69
+ sessionId: z.uuid().optional(),
69
70
  })
70
71
  .safeParse(c.req.query());
71
72
  if (!query.success) throw new HTTPException(422, { message: "Invalid Skill list query" });
73
+ if (query.data.sessionId) {
74
+ await requireAccessGrant(c, deps, workspaceId, "sessions:read");
75
+ const session = await getSessionForSubject(
76
+ deps.db,
77
+ workspaceId,
78
+ query.data.sessionId,
79
+ grant.subjectId,
80
+ );
81
+ if (!session) throw new HTTPException(404, { message: "Session not found" });
82
+ }
72
83
  let after: { stableKey: string; id: string } | undefined;
73
84
  if (query.data.cursor !== undefined) {
74
85
  try {
@@ -83,7 +94,12 @@ export function registerSkillContentRoutes(app: Hono, deps: ApiRouteDeps): void
83
94
  const rows = await listSkills(
84
95
  deps.db,
85
96
  { accountId: grant.accountId, workspaceId, subjectId: grant.subjectId },
86
- { limit: query.data.limit + 1, metadataOnly: true, ...(after ? { after } : {}) },
97
+ {
98
+ limit: query.data.limit + 1,
99
+ metadataOnly: true,
100
+ ...(after ? { after } : {}),
101
+ ...(query.data.sessionId ? { sessionId: query.data.sessionId } : {}),
102
+ },
87
103
  );
88
104
  const page = rows.slice(0, query.data.limit);
89
105
  const last = page.at(-1);
@@ -33,7 +33,7 @@ import {
33
33
  type MachineMetricsRow,
34
34
  } from "@opengeni/db";
35
35
  import { MachineView, MetricSample, type MachinesResponse } from "@opengeni/contracts";
36
- import { managedSessionGroupBackend } from "@opengeni/core";
36
+ import { managedSessionGroupBackend, sessionGroupMachinePresentation } from "@opengeni/core";
37
37
  import { selfhostedHeartbeatLiveness } from "@opengeni/runtime/sandbox";
38
38
 
39
39
  export type MachinesServices = {
@@ -258,23 +258,24 @@ export async function listMachines(
258
258
  if (session && groupBackend) {
259
259
  const groupActive = activeSandboxId === null;
260
260
  const groupLease = await readLease(db, workspaceId, session.sandboxGroupId);
261
+ const presentation = sessionGroupMachinePresentation(groupBackend);
262
+ const localHost = groupBackend === "local";
261
263
  machines.push(
262
264
  MachineView.parse({
263
265
  sandboxId: session.sandboxGroupId,
264
266
  enrollmentId: null,
265
- name: "session sandbox",
266
- kind: groupBackend === "opensandbox" ? "opensandbox" : "modal",
267
+ name: presentation.name,
268
+ kind: presentation.kind,
267
269
  state: "online",
268
270
  active: groupActive,
269
271
  isSessionGroup: true,
270
272
  workspaceGeneration: groupLease?.workspaceGeneration ?? null,
271
273
  archiveGeneration: groupLease?.archiveGeneration ?? null,
272
274
  archiveComplete: groupLease?.archiveComplete ?? false,
273
- // The Modal group box is a cloud Linux box; its precise OS/arch is not
274
- // surfaced as a metric, so the dashboard shows the canonical linux/x86_64.
275
- os: "linux",
276
- arch: "x86_64",
277
- hasDisplay: false,
275
+ // A local sandbox is this machine. Other managed providers run a Linux box.
276
+ os: localHost ? process.platform : "linux",
277
+ arch: localHost ? process.arch : "x86_64",
278
+ hasDisplay: localHost,
278
279
  desktopUnavailableReason: null,
279
280
  allowScreenControl: false,
280
281
  sharedSessionCount: 1,
@@ -295,6 +295,14 @@ export function sandboxFileContentType(filename: string): string {
295
295
  ".jpg": "image/jpeg",
296
296
  ".json": "application/json",
297
297
  ".md": "text/markdown",
298
+ ".mp4": "video/mp4",
299
+ ".webm": "video/webm",
300
+ ".ogv": "video/ogg",
301
+ ".mp3": "audio/mpeg",
302
+ ".m4a": "audio/mp4",
303
+ ".ogg": "audio/ogg",
304
+ ".wav": "audio/wav",
305
+ ".flac": "audio/flac",
298
306
  ".pdf": "application/pdf",
299
307
  ".png": "image/png",
300
308
  ".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
@@ -314,7 +322,13 @@ export function sandboxArtifactIdentity(input: {
314
322
  sha256: string;
315
323
  }): { fileId: string; uploadId: string } {
316
324
  const digest = createHash("sha256")
317
- .update("opengeni-sandbox-file-artifact-v1\0")
325
+ // Newly classified media must not collide with old binary-typed publications.
326
+ // Preserve the v1 identity for every previously supported format.
327
+ .update(
328
+ /^(audio|video)\//.test(sandboxFileContentType(input.path))
329
+ ? "opengeni-sandbox-media-artifact-v1\0"
330
+ : "opengeni-sandbox-file-artifact-v1\0",
331
+ )
318
332
  .update(input.workspaceId)
319
333
  .update("\0")
320
334
  .update(input.sessionId)
@@ -1,5 +1,6 @@
1
1
  import { WorkspaceArtifactSourceBundle, type AccessGrant } from "@opengeni/contracts";
2
2
  import type { ApiRouteDeps, ObjectStorageDependency } from "@opengeni/core";
3
+ import { retryWhileMissing } from "@opengeni/storage";
3
4
  import {
4
5
  createWorkspaceArtifactUpload,
5
6
  getWorkspaceArtifactUpload,
@@ -19,10 +20,15 @@ export async function validateSiteSource(storage: Storage, key: string, sizeByte
19
20
  "Editable source JSON must be at most 64 MiB. Exclude dependencies and build output; HTML has a separate storage limit.",
20
21
  );
21
22
  }
22
- const content = await storage.getObjectBytes(key);
23
+ const content = await retryWhileMissing(() => storage.getObjectBytes(key));
24
+ if (!content) {
25
+ throw new WorkspaceArtifactOperationError(
26
+ "Site source is not available yet. Retry publication with the same upload.",
27
+ );
28
+ }
23
29
  try {
24
30
  WorkspaceArtifactSourceBundle.parse(
25
- JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(content!.bytes)),
31
+ JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(content.bytes)),
26
32
  );
27
33
  } catch {
28
34
  throw new WorkspaceArtifactOperationError(
@@ -129,13 +135,22 @@ async function freeze(
129
135
  }
130
136
  decoder.decode();
131
137
  }
132
- await storage.putObjectStreamIfAbsent({
138
+ const created = await storage.putObjectStreamIfAbsent({
133
139
  key: to,
134
140
  contentType,
135
141
  byteSize: head.ContentLength,
136
142
  chunks: chunks(),
137
143
  });
138
- return (await storage.headObject(to))!.ContentLength!;
144
+ // A successful conditional PUT is the receipt. A following visibility miss
145
+ // cannot invalidate it. Only a conflict needs the existing winner's metadata.
146
+ if (created) return head.ContentLength;
147
+ const stored = await retryWhileMissing(() => storage.headObject!(to));
148
+ if (!stored || stored.ContentLength === undefined) {
149
+ throw new WorkspaceArtifactOperationError(
150
+ "Frozen Site upload is unavailable. Retry publication with the same upload.",
151
+ );
152
+ }
153
+ return stored.ContentLength;
139
154
  }
140
155
  type Input = Partial<Parameters<typeof prepareWorkspaceArtifactContent>[2]> & { uploadId?: string };
141
156
  export async function prepareWorkspaceArtifactPublication(