@opengeni/api-router 2.10.0-canary.0 → 2.10.0-canary.2
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-XXH7DW34.js → chunk-BVX5ASFY.js} +1559 -798
- package/dist/chunk-BVX5ASFY.js.map +1 -0
- package/dist/codemode.d.ts +3 -0
- package/dist/index.js +1 -1
- package/dist/integrations/slack-interactions.d.ts +1 -1
- package/dist/mcp/command-read.d.ts +24 -0
- package/dist/mcp/scheduled-task-view.d.ts +3 -3
- package/dist/mcp/server.d.ts +2 -0
- package/dist/mcp/session-event-view.d.ts +86 -0
- package/dist/sandbox/channel-a.d.ts +2 -0
- package/dist/sandbox/rematerialize.d.ts +2 -0
- package/dist/sandbox/viewer.d.ts +2 -0
- package/dist/session-event-view-MU3HMNV3.js +264 -0
- package/dist/session-event-view-MU3HMNV3.js.map +1 -0
- package/dist/site-uploads.d.ts +41 -0
- package/dist/workspace-artifact-content.d.ts +4 -4
- package/package.json +19 -19
- package/src/app.ts +20 -0
- package/src/codemode.ts +52 -0
- package/src/integrations/oauth-client.ts +51 -7
- package/src/mcp/command-read.ts +109 -0
- package/src/mcp/server.ts +287 -153
- package/src/mcp/session-event-view.ts +336 -0
- package/src/mcp/session-wait.ts +1 -1
- package/src/routes/github.ts +124 -1
- package/src/routes/scheduled-tasks.ts +19 -1
- package/src/routes/sessions.ts +150 -7
- package/src/routes/workspace-artifacts.ts +92 -21
- package/src/sandbox/channel-a.ts +5 -0
- package/src/sandbox/rematerialize.ts +46 -3
- package/src/sandbox/viewer.ts +3 -0
- package/src/site-uploads.ts +202 -0
- package/src/workspace-artifact-content.ts +20 -39
- package/dist/chunk-XXH7DW34.js.map +0 -1
package/src/routes/sessions.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { scheduledSessionIds } from "@opengeni/db";
|
|
1
2
|
import {
|
|
2
3
|
AcknowledgeStreamRequest,
|
|
3
4
|
ApplySessionGoalRevisionRequest,
|
|
@@ -335,6 +336,7 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
335
336
|
db,
|
|
336
337
|
settings,
|
|
337
338
|
bus,
|
|
339
|
+
objectStorage,
|
|
338
340
|
observability: deps.observability,
|
|
339
341
|
};
|
|
340
342
|
const workspaceCaptureManifestCache = new WorkspaceCaptureManifestCache();
|
|
@@ -516,6 +518,7 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
516
518
|
db,
|
|
517
519
|
settings,
|
|
518
520
|
bus,
|
|
521
|
+
objectStorage,
|
|
519
522
|
...(deps.establishSandboxSession
|
|
520
523
|
? { establishSandboxSession: deps.establishSandboxSession }
|
|
521
524
|
: {}),
|
|
@@ -666,6 +669,12 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
666
669
|
...(query.pinsOnly ? { pinsOnly: true } : {}),
|
|
667
670
|
...(query.archivedOnly ? { archivedOnly: true } : {}),
|
|
668
671
|
...(query.parentSessionId !== undefined ? { parentSessionId: query.parentSessionId } : {}),
|
|
672
|
+
...(query.channelId !== undefined ? { channelId: query.channelId } : {}),
|
|
673
|
+
...(query.createdBy ? { createdBy: query.createdBy } : {}),
|
|
674
|
+
...(query.updatedFrom ? { updatedFrom: query.updatedFrom } : {}),
|
|
675
|
+
...(query.updatedBefore ? { updatedBefore: query.updatedBefore } : {}),
|
|
676
|
+
...(query.createdFrom ? { createdFrom: query.createdFrom } : {}),
|
|
677
|
+
...(query.createdBefore ? { createdBefore: query.createdBefore } : {}),
|
|
669
678
|
...(authorizationScope ? { authorizationScope } : {}),
|
|
670
679
|
// A managed human's own personal workspace has no membership row, so
|
|
671
680
|
// the list's removal fence must fall back to the organization-membership
|
|
@@ -698,10 +707,23 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
698
707
|
workspaceId,
|
|
699
708
|
sessionIds: [...page.pinned, ...page.sessions].map((session) => session.id),
|
|
700
709
|
});
|
|
710
|
+
const scheduleTargets =
|
|
711
|
+
hasPermission(grant.permissions, "scheduled_tasks:run") &&
|
|
712
|
+
hasPermission(grant.permissions, "sessions:control")
|
|
713
|
+
? await scheduledSessionIds(
|
|
714
|
+
db,
|
|
715
|
+
workspaceId,
|
|
716
|
+
[...page.pinned, ...page.sessions].map((session) => session.id),
|
|
717
|
+
)
|
|
718
|
+
: new Set<string>();
|
|
701
719
|
const decorate = (session: Session): Session => {
|
|
702
720
|
const activity = commandActivity.get(session.id);
|
|
703
721
|
return sessionWithEffectiveToolPolicy(
|
|
704
|
-
{
|
|
722
|
+
{
|
|
723
|
+
...session,
|
|
724
|
+
hasSchedules: scheduleTargets.has(session.id),
|
|
725
|
+
...(activity ? { backgroundCommandActivity: activity } : {}),
|
|
726
|
+
},
|
|
705
727
|
policy.workspaceServerIds,
|
|
706
728
|
policy.workspaceDefaultServerIds,
|
|
707
729
|
);
|
|
@@ -709,6 +731,7 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
709
731
|
if (pageView) {
|
|
710
732
|
return c.json({
|
|
711
733
|
...page,
|
|
734
|
+
...(query.hasPageFilters ? { filtersApplied: true as const } : {}),
|
|
712
735
|
pinned: page.pinned.map(decorate),
|
|
713
736
|
sessions: page.sessions.map(decorate),
|
|
714
737
|
});
|
|
@@ -888,7 +911,23 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
888
911
|
if (!session) {
|
|
889
912
|
throw new HTTPException(404, { message: "session not found" });
|
|
890
913
|
}
|
|
891
|
-
|
|
914
|
+
const activity = await backgroundCommandActivityForSessions(db, {
|
|
915
|
+
accountId: grant.accountId,
|
|
916
|
+
workspaceId,
|
|
917
|
+
sessionIds: [sessionId],
|
|
918
|
+
});
|
|
919
|
+
const scheduleTargets =
|
|
920
|
+
hasPermission(grant.permissions, "scheduled_tasks:run") &&
|
|
921
|
+
hasPermission(grant.permissions, "sessions:control")
|
|
922
|
+
? await scheduledSessionIds(db, workspaceId, [sessionId])
|
|
923
|
+
: new Set<string>();
|
|
924
|
+
return c.json(
|
|
925
|
+
await withEffectivePolicy(deps, workspaceId, grant.subjectId, {
|
|
926
|
+
...session,
|
|
927
|
+
hasSchedules: scheduleTargets.has(sessionId),
|
|
928
|
+
...(activity.get(sessionId) ? { backgroundCommandActivity: activity.get(sessionId) } : {}),
|
|
929
|
+
}),
|
|
930
|
+
);
|
|
892
931
|
});
|
|
893
932
|
|
|
894
933
|
app.get("/v1/workspaces/:workspaceId/sessions/:sessionId/model-context", async (c) => {
|
|
@@ -925,6 +964,7 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
925
964
|
accountId: grant.accountId,
|
|
926
965
|
workspaceId,
|
|
927
966
|
sessionId,
|
|
967
|
+
activeOnly: true,
|
|
928
968
|
});
|
|
929
969
|
return c.json({ commands });
|
|
930
970
|
});
|
|
@@ -1803,17 +1843,40 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
1803
1843
|
const workspaceId = c.req.param("workspaceId");
|
|
1804
1844
|
const grant = await requireAccessGrant(c, deps, workspaceId, "sessions:read");
|
|
1805
1845
|
const lineage = await readSessionLineage(deps, grant, c.req.param("sessionId"));
|
|
1846
|
+
const sessionIds = [
|
|
1847
|
+
c.req.param("sessionId"),
|
|
1848
|
+
...lineage.ancestors.map((session) => session.id),
|
|
1849
|
+
];
|
|
1850
|
+
const collect = (nodes: LineageNode[]) => {
|
|
1851
|
+
for (const node of nodes) {
|
|
1852
|
+
sessionIds.push(node.session.id);
|
|
1853
|
+
collect(node.children);
|
|
1854
|
+
}
|
|
1855
|
+
};
|
|
1856
|
+
collect(lineage.children);
|
|
1857
|
+
const targets =
|
|
1858
|
+
hasPermission(grant.permissions, "scheduled_tasks:run") &&
|
|
1859
|
+
hasPermission(grant.permissions, "sessions:control")
|
|
1860
|
+
? await scheduledSessionIds(db, workspaceId, sessionIds)
|
|
1861
|
+
: new Set<string>();
|
|
1862
|
+
const decorateNodes = (nodes: LineageNode[]): LineageNode[] =>
|
|
1863
|
+
nodes.map((node) => ({
|
|
1864
|
+
...node,
|
|
1865
|
+
session: { ...node.session, hasSchedules: targets.has(node.session.id) },
|
|
1866
|
+
children: decorateNodes(node.children),
|
|
1867
|
+
}));
|
|
1806
1868
|
const policy = await loadEffectivePolicyContext(deps, workspaceId, grant.subjectId);
|
|
1807
1869
|
return c.json({
|
|
1808
1870
|
...lineage,
|
|
1871
|
+
sessionHasSchedules: targets.has(c.req.param("sessionId")),
|
|
1809
1872
|
ancestors: lineage.ancestors.map((session) =>
|
|
1810
1873
|
sessionWithEffectiveToolPolicy(
|
|
1811
|
-
session,
|
|
1874
|
+
{ ...session, hasSchedules: targets.has(session.id) },
|
|
1812
1875
|
policy.workspaceServerIds,
|
|
1813
1876
|
policy.workspaceDefaultServerIds,
|
|
1814
1877
|
),
|
|
1815
1878
|
),
|
|
1816
|
-
children: mapLineageNodes(lineage.children, policy),
|
|
1879
|
+
children: decorateNodes(mapLineageNodes(lineage.children, policy)),
|
|
1817
1880
|
});
|
|
1818
1881
|
});
|
|
1819
1882
|
|
|
@@ -4617,6 +4680,13 @@ function sessionListQuery(
|
|
|
4617
4680
|
search: string | undefined;
|
|
4618
4681
|
pinsOnly: boolean;
|
|
4619
4682
|
archivedOnly: boolean;
|
|
4683
|
+
channelId: string | null | undefined;
|
|
4684
|
+
createdBy: { kind: "subject" | "service"; subjectId: string } | undefined;
|
|
4685
|
+
updatedFrom: Date | undefined;
|
|
4686
|
+
updatedBefore: Date | undefined;
|
|
4687
|
+
createdFrom: Date | undefined;
|
|
4688
|
+
createdBefore: Date | undefined;
|
|
4689
|
+
hasPageFilters: boolean;
|
|
4620
4690
|
} {
|
|
4621
4691
|
const parentSessionId = query.parentSessionId;
|
|
4622
4692
|
// "null" = roots only; a uuid = children of that session; anything else is
|
|
@@ -4652,12 +4722,73 @@ function sessionListQuery(
|
|
|
4652
4722
|
throw new HTTPException(400, { message: 'archivedOnly must be the literal "true"' });
|
|
4653
4723
|
}
|
|
4654
4724
|
const archivedOnly = query.archivedOnly === "true";
|
|
4725
|
+
const channelId = query.channelId;
|
|
4726
|
+
if (
|
|
4727
|
+
channelId !== undefined &&
|
|
4728
|
+
channelId !== "null" &&
|
|
4729
|
+
!z.string().uuid().safeParse(channelId).success
|
|
4730
|
+
) {
|
|
4731
|
+
throw new HTTPException(400, {
|
|
4732
|
+
message: 'channelId must be a channel id or the literal "null"',
|
|
4733
|
+
});
|
|
4734
|
+
}
|
|
4735
|
+
const createdByKind = query.createdByKind;
|
|
4736
|
+
const createdBySubjectId = query.createdBySubjectId;
|
|
4737
|
+
if ((createdByKind === undefined) !== (createdBySubjectId === undefined)) {
|
|
4738
|
+
throw new HTTPException(400, {
|
|
4739
|
+
message: "createdByKind and createdBySubjectId must be supplied together",
|
|
4740
|
+
});
|
|
4741
|
+
}
|
|
4742
|
+
if (createdByKind !== undefined && createdByKind !== "subject" && createdByKind !== "service") {
|
|
4743
|
+
throw new HTTPException(400, {
|
|
4744
|
+
message: 'createdByKind must be "subject" or "service"',
|
|
4745
|
+
});
|
|
4746
|
+
}
|
|
4747
|
+
if (
|
|
4748
|
+
createdBySubjectId !== undefined &&
|
|
4749
|
+
(createdBySubjectId.trim().length < 1 || createdBySubjectId.length > 1_024)
|
|
4750
|
+
) {
|
|
4751
|
+
throw new HTTPException(400, {
|
|
4752
|
+
message: "createdBySubjectId must be between 1 and 1024 characters",
|
|
4753
|
+
});
|
|
4754
|
+
}
|
|
4755
|
+
const parseDateBound = (name: string): Date | undefined => {
|
|
4756
|
+
const raw = query[name];
|
|
4757
|
+
if (raw === undefined) return undefined;
|
|
4758
|
+
const parsed = z.string().datetime({ offset: true }).safeParse(raw);
|
|
4759
|
+
if (!parsed.success) {
|
|
4760
|
+
throw new HTTPException(400, { message: `${name} must be an ISO-8601 timestamp` });
|
|
4761
|
+
}
|
|
4762
|
+
// Date-backed filters must not silently round accepted microseconds to an
|
|
4763
|
+
// earlier boundary. Clients can use any ISO offset with at most 3 decimals.
|
|
4764
|
+
if (/\.\d{4,}/.test(parsed.data)) {
|
|
4765
|
+
throw new HTTPException(400, { message: `${name} supports at most millisecond precision` });
|
|
4766
|
+
}
|
|
4767
|
+
return new Date(parsed.data);
|
|
4768
|
+
};
|
|
4769
|
+
const updatedFrom = parseDateBound("updatedFrom");
|
|
4770
|
+
const updatedBefore = parseDateBound("updatedBefore");
|
|
4771
|
+
const createdFrom = parseDateBound("createdFrom");
|
|
4772
|
+
const createdBefore = parseDateBound("createdBefore");
|
|
4773
|
+
if (updatedFrom && updatedBefore && updatedFrom >= updatedBefore) {
|
|
4774
|
+
throw new HTTPException(400, { message: "updatedFrom must be earlier than updatedBefore" });
|
|
4775
|
+
}
|
|
4776
|
+
if (createdFrom && createdBefore && createdFrom >= createdBefore) {
|
|
4777
|
+
throw new HTTPException(400, { message: "createdFrom must be earlier than createdBefore" });
|
|
4778
|
+
}
|
|
4779
|
+
const hasPageFilters =
|
|
4780
|
+
channelId !== undefined ||
|
|
4781
|
+
createdByKind !== undefined ||
|
|
4782
|
+
updatedFrom !== undefined ||
|
|
4783
|
+
updatedBefore !== undefined ||
|
|
4784
|
+
createdFrom !== undefined ||
|
|
4785
|
+
createdBefore !== undefined;
|
|
4655
4786
|
if (pinsOnly && !allowCursor) {
|
|
4656
4787
|
throw new HTTPException(400, { message: 'pinsOnly requires view="page"' });
|
|
4657
4788
|
}
|
|
4658
|
-
if (pinsOnly && (rawCursor || parentSessionId !== undefined || search)) {
|
|
4789
|
+
if (pinsOnly && (rawCursor || parentSessionId !== undefined || search || hasPageFilters)) {
|
|
4659
4790
|
throw new HTTPException(400, {
|
|
4660
|
-
message: "pinsOnly cannot be combined with cursor, parentSessionId, or
|
|
4791
|
+
message: "pinsOnly cannot be combined with cursor, parentSessionId, search, or filters",
|
|
4661
4792
|
});
|
|
4662
4793
|
}
|
|
4663
4794
|
if (pinsOnly && archivedOnly) {
|
|
@@ -4675,6 +4806,16 @@ function sessionListQuery(
|
|
|
4675
4806
|
search: search || undefined,
|
|
4676
4807
|
pinsOnly,
|
|
4677
4808
|
archivedOnly,
|
|
4809
|
+
channelId: channelId === undefined ? undefined : channelId === "null" ? null : channelId,
|
|
4810
|
+
createdBy:
|
|
4811
|
+
createdByKind && createdBySubjectId
|
|
4812
|
+
? { kind: createdByKind, subjectId: createdBySubjectId }
|
|
4813
|
+
: undefined,
|
|
4814
|
+
updatedFrom,
|
|
4815
|
+
updatedBefore,
|
|
4816
|
+
createdFrom,
|
|
4817
|
+
createdBefore,
|
|
4818
|
+
hasPageFilters,
|
|
4678
4819
|
};
|
|
4679
4820
|
}
|
|
4680
4821
|
|
|
@@ -4934,9 +5075,11 @@ export function agentTopologyQuery(query: Record<string, string>): {
|
|
|
4934
5075
|
throw new HTTPException(400, { message: "query cannot be combined with an exact subject" });
|
|
4935
5076
|
}
|
|
4936
5077
|
const relevanceRequested = Boolean(searchQuery || subject);
|
|
5078
|
+
// An explicit root scopes the whole workstream; only an explicit parent
|
|
5079
|
+
// narrows it to one level. Ordinary browse still defaults to root sessions.
|
|
4937
5080
|
const parentSessionId =
|
|
4938
5081
|
rawParent === undefined
|
|
4939
|
-
? relevanceRequested
|
|
5082
|
+
? relevanceRequested || Boolean(rootSessionId)
|
|
4940
5083
|
? undefined
|
|
4941
5084
|
: null
|
|
4942
5085
|
: rawParent === "null"
|
|
@@ -36,16 +36,20 @@ import {
|
|
|
36
36
|
import type { Context, Hono } from "hono";
|
|
37
37
|
import { HTTPException } from "hono/http-exception";
|
|
38
38
|
import { z } from "zod";
|
|
39
|
-
import {
|
|
40
|
-
prepareWorkspaceArtifactContent,
|
|
41
|
-
readWorkspaceArtifactContent,
|
|
42
|
-
} from "../workspace-artifact-content";
|
|
39
|
+
import { readWorkspaceArtifactContent } from "../workspace-artifact-content";
|
|
43
40
|
import {
|
|
44
41
|
projectWorkspaceArtifactDetailProvenance,
|
|
45
42
|
projectWorkspaceArtifactMutationProvenance,
|
|
46
43
|
redactWorkspaceArtifactListProvenance,
|
|
47
44
|
} from "../workspace-artifact-provenance";
|
|
48
45
|
|
|
46
|
+
import {
|
|
47
|
+
prepareWorkspaceArtifactPublication,
|
|
48
|
+
prepareWorkspaceArtifactUpload,
|
|
49
|
+
workspaceArtifactDownloads,
|
|
50
|
+
readArtifactObject,
|
|
51
|
+
} from "../site-uploads";
|
|
52
|
+
|
|
49
53
|
const ArtifactId = z.string().uuid();
|
|
50
54
|
|
|
51
55
|
async function body<S extends z.ZodType>(context: Context, schema: S): Promise<z.infer<S>> {
|
|
@@ -54,6 +58,13 @@ async function body<S extends z.ZodType>(context: Context, schema: S): Promise<z
|
|
|
54
58
|
return parsed.data;
|
|
55
59
|
}
|
|
56
60
|
|
|
61
|
+
function parseVersionId(value: string | undefined): string | undefined {
|
|
62
|
+
if (value !== undefined && !ArtifactId.safeParse(value).success) {
|
|
63
|
+
throw new HTTPException(422, { message: "Invalid artifact version id" });
|
|
64
|
+
}
|
|
65
|
+
return value;
|
|
66
|
+
}
|
|
67
|
+
|
|
57
68
|
function artifactId(context: Context): string {
|
|
58
69
|
const parsed = ArtifactId.safeParse(context.req.param("artifactId"));
|
|
59
70
|
if (!parsed.success) throw new HTTPException(422, { message: "Invalid artifact id" });
|
|
@@ -94,14 +105,14 @@ export function workspaceArtifactErrorResponse(context: Context, error: unknown)
|
|
|
94
105
|
|
|
95
106
|
function prepareContent(
|
|
96
107
|
deps: ApiRouteDeps,
|
|
97
|
-
|
|
98
|
-
input: Parameters<typeof
|
|
108
|
+
grant: AccessGrant,
|
|
109
|
+
input: Parameters<typeof prepareWorkspaceArtifactPublication>[2],
|
|
99
110
|
) {
|
|
100
111
|
if (!deps.objectStorage)
|
|
101
112
|
throw new HTTPException(503, {
|
|
102
113
|
message: "Object storage is not configured",
|
|
103
114
|
});
|
|
104
|
-
return
|
|
115
|
+
return prepareWorkspaceArtifactPublication(deps, grant, input);
|
|
105
116
|
}
|
|
106
117
|
|
|
107
118
|
function provenance(subjectId: string, idempotencyKey: string) {
|
|
@@ -156,6 +167,30 @@ export function registerWorkspaceArtifactRoutes(app: Hono, deps: ApiRouteDeps):
|
|
|
156
167
|
// retained-output API. The product route remains simply `/artifacts`.
|
|
157
168
|
const base = "/v1/workspaces/:workspaceId/published-artifacts";
|
|
158
169
|
|
|
170
|
+
app.post(`${base}/uploads`, async (context) => {
|
|
171
|
+
const grant = await requireAccessGrant(
|
|
172
|
+
context,
|
|
173
|
+
deps,
|
|
174
|
+
context.req.param("workspaceId"),
|
|
175
|
+
"artifacts:publish",
|
|
176
|
+
);
|
|
177
|
+
return context.json(await prepareWorkspaceArtifactUpload(deps, grant));
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
app.get(`${base}/:artifactId/downloads`, async (context) => {
|
|
181
|
+
const workspaceId = context.req.param("workspaceId");
|
|
182
|
+
await requireAccessGrant(context, deps, workspaceId, "artifacts:read");
|
|
183
|
+
if (!deps.objectStorage)
|
|
184
|
+
throw new HTTPException(503, { message: "Object storage is not configured" });
|
|
185
|
+
const ref = await getWorkspaceArtifactContentRef(
|
|
186
|
+
deps.db,
|
|
187
|
+
workspaceId,
|
|
188
|
+
artifactId(context),
|
|
189
|
+
parseVersionId(context.req.query("versionId")),
|
|
190
|
+
);
|
|
191
|
+
return context.json(await workspaceArtifactDownloads(deps.objectStorage, ref, "public"));
|
|
192
|
+
});
|
|
193
|
+
|
|
159
194
|
app.get(base, async (context) => {
|
|
160
195
|
const workspaceId = context.req.param("workspaceId");
|
|
161
196
|
await requireAccessGrant(context, deps, workspaceId, "artifacts:read");
|
|
@@ -195,12 +230,13 @@ export function registerWorkspaceArtifactRoutes(app: Hono, deps: ApiRouteDeps):
|
|
|
195
230
|
const id = crypto.randomUUID();
|
|
196
231
|
const slugBase = request.slug ?? (normalizeWorkspaceArtifactSlug(request.title) || "artifact");
|
|
197
232
|
const slug = request.slug ?? `${slugBase.slice(0, 87)}-${id.slice(0, 8)}`;
|
|
198
|
-
const content = prepareContent(deps, workspaceId, {
|
|
199
|
-
html: request.html,
|
|
200
|
-
...(request.source ? { source: request.source } : {}),
|
|
201
|
-
...(request.requestedTools ? { requestedTools: request.requestedTools } : {}),
|
|
202
|
-
});
|
|
203
233
|
try {
|
|
234
|
+
const content = await prepareContent(deps, grant, {
|
|
235
|
+
...(request.html !== undefined ? { html: request.html } : {}),
|
|
236
|
+
...(request.uploadId ? { uploadId: request.uploadId } : {}),
|
|
237
|
+
...(request.source ? { source: request.source } : {}),
|
|
238
|
+
...(request.requestedTools ? { requestedTools: request.requestedTools } : {}),
|
|
239
|
+
});
|
|
204
240
|
return context.json(
|
|
205
241
|
await mutationResponse(
|
|
206
242
|
deps,
|
|
@@ -241,6 +277,43 @@ export function registerWorkspaceArtifactRoutes(app: Hono, deps: ApiRouteDeps):
|
|
|
241
277
|
}
|
|
242
278
|
});
|
|
243
279
|
|
|
280
|
+
app.get(`${base}/:artifactId/html`, async (context) => {
|
|
281
|
+
const workspaceId = context.req.param("workspaceId");
|
|
282
|
+
await requireAccessGrant(context, deps, workspaceId, "artifacts:read");
|
|
283
|
+
if (!deps.objectStorage)
|
|
284
|
+
throw new HTTPException(503, { message: "Object storage is not configured" });
|
|
285
|
+
const ref = await getWorkspaceArtifactContentRef(
|
|
286
|
+
deps.db,
|
|
287
|
+
workspaceId,
|
|
288
|
+
artifactId(context),
|
|
289
|
+
parseVersionId(context.req.query("versionId")),
|
|
290
|
+
);
|
|
291
|
+
const iterator = readArtifactObject(deps.objectStorage, ref.contentKey);
|
|
292
|
+
return new Response(
|
|
293
|
+
new ReadableStream({
|
|
294
|
+
async pull(controller) {
|
|
295
|
+
try {
|
|
296
|
+
const item = await iterator.next();
|
|
297
|
+
if (item.done) controller.close();
|
|
298
|
+
else controller.enqueue(item.value);
|
|
299
|
+
} catch (error) {
|
|
300
|
+
controller.error(error);
|
|
301
|
+
}
|
|
302
|
+
},
|
|
303
|
+
async cancel() {
|
|
304
|
+
await iterator.return();
|
|
305
|
+
},
|
|
306
|
+
}),
|
|
307
|
+
{
|
|
308
|
+
headers: {
|
|
309
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
310
|
+
"Cache-Control": "private, no-store",
|
|
311
|
+
"Content-Security-Policy": "sandbox allow-scripts",
|
|
312
|
+
},
|
|
313
|
+
},
|
|
314
|
+
);
|
|
315
|
+
});
|
|
316
|
+
|
|
244
317
|
app.get(`${base}/:artifactId/content`, async (context) => {
|
|
245
318
|
const workspaceId = context.req.param("workspaceId");
|
|
246
319
|
await requireAccessGrant(context, deps, workspaceId, "artifacts:read");
|
|
@@ -248,10 +321,7 @@ export function registerWorkspaceArtifactRoutes(app: Hono, deps: ApiRouteDeps):
|
|
|
248
321
|
throw new HTTPException(503, {
|
|
249
322
|
message: "Object storage is not configured",
|
|
250
323
|
});
|
|
251
|
-
const parsedVersion = context.req.query("versionId");
|
|
252
|
-
if (parsedVersion && !ArtifactId.safeParse(parsedVersion).success) {
|
|
253
|
-
throw new HTTPException(422, { message: "Invalid artifact version id" });
|
|
254
|
-
}
|
|
324
|
+
const parsedVersion = parseVersionId(context.req.query("versionId"));
|
|
255
325
|
try {
|
|
256
326
|
const ref = await getWorkspaceArtifactContentRef(
|
|
257
327
|
deps.db,
|
|
@@ -293,12 +363,13 @@ export function registerWorkspaceArtifactRoutes(app: Hono, deps: ApiRouteDeps):
|
|
|
293
363
|
const grant = authorization.grant;
|
|
294
364
|
const request = await body(context, PublishWorkspaceArtifactVersionRequest);
|
|
295
365
|
const id = artifactId(context);
|
|
296
|
-
const content = prepareContent(deps, workspaceId, {
|
|
297
|
-
html: request.html,
|
|
298
|
-
...(request.source ? { source: request.source } : {}),
|
|
299
|
-
...(request.requestedTools ? { requestedTools: request.requestedTools } : {}),
|
|
300
|
-
});
|
|
301
366
|
try {
|
|
367
|
+
const content = await prepareContent(deps, grant, {
|
|
368
|
+
...(request.html !== undefined ? { html: request.html } : {}),
|
|
369
|
+
...(request.uploadId ? { uploadId: request.uploadId } : {}),
|
|
370
|
+
...(request.source ? { source: request.source } : {}),
|
|
371
|
+
...(request.requestedTools ? { requestedTools: request.requestedTools } : {}),
|
|
372
|
+
});
|
|
302
373
|
return context.json(
|
|
303
374
|
await mutationResponse(
|
|
304
375
|
deps,
|
package/src/sandbox/channel-a.ts
CHANGED
|
@@ -55,6 +55,7 @@ import {
|
|
|
55
55
|
} from "@opengeni/observability";
|
|
56
56
|
import { HTTPException } from "hono/http-exception";
|
|
57
57
|
import { ApiHttpError } from "../http/api-error";
|
|
58
|
+
import type { ObjectStorage } from "@opengeni/storage";
|
|
58
59
|
|
|
59
60
|
import {
|
|
60
61
|
buildSelfhostedBackendSession,
|
|
@@ -101,6 +102,7 @@ export type ChannelAServices = {
|
|
|
101
102
|
db: Database;
|
|
102
103
|
settings: Settings;
|
|
103
104
|
bus: EventBus;
|
|
105
|
+
objectStorage?: ObjectStorage | null;
|
|
104
106
|
observability?: Observability | undefined;
|
|
105
107
|
};
|
|
106
108
|
|
|
@@ -1012,6 +1014,9 @@ async function withChannelAOperation<T>(
|
|
|
1012
1014
|
acquiredLease: acquired.lease,
|
|
1013
1015
|
fallbackEnvelope: envelope,
|
|
1014
1016
|
dataPlaneUrl: acquired.lease.dataPlaneUrl,
|
|
1017
|
+
...(services.objectStorage !== undefined
|
|
1018
|
+
? { objectStorage: services.objectStorage }
|
|
1019
|
+
: {}),
|
|
1015
1020
|
});
|
|
1016
1021
|
established = result.established;
|
|
1017
1022
|
leaseSnapshot = result.lease;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Settings } from "@opengeni/config";
|
|
2
|
+
import { parseWorkspaceArchiveObjectRef } from "@opengeni/contracts";
|
|
2
3
|
import {
|
|
3
4
|
adoptLegacyModalCheckpointArtifact,
|
|
4
5
|
beginSandboxRematerialization,
|
|
@@ -13,6 +14,7 @@ import {
|
|
|
13
14
|
type LeaseSnapshot,
|
|
14
15
|
} from "@opengeni/db";
|
|
15
16
|
import {
|
|
17
|
+
inlineWorkspaceArchiveForRestore,
|
|
16
18
|
describeLegacyNativeSnapshotArchive,
|
|
17
19
|
establishSandboxSessionFromEnvelope,
|
|
18
20
|
isProviderSandboxNotFoundError,
|
|
@@ -30,17 +32,55 @@ import {
|
|
|
30
32
|
type EstablishedSandboxSession,
|
|
31
33
|
type WorkspaceArchiveDescriptor,
|
|
32
34
|
} from "@opengeni/runtime/sandbox";
|
|
35
|
+
import type { ObjectStorage } from "@opengeni/storage";
|
|
33
36
|
|
|
34
37
|
function hasWorkspaceArchive(envelope: Record<string, unknown> | null): boolean {
|
|
35
38
|
const sessionState =
|
|
36
|
-
envelope?.sessionState &&
|
|
39
|
+
envelope?.sessionState &&
|
|
40
|
+
typeof envelope.sessionState === "object" &&
|
|
41
|
+
!Array.isArray(envelope.sessionState)
|
|
37
42
|
? (envelope.sessionState as Record<string, unknown>)
|
|
38
43
|
: null;
|
|
39
44
|
return (
|
|
40
|
-
typeof sessionState?.workspaceArchive === "string" &&
|
|
45
|
+
(typeof sessionState?.workspaceArchive === "string" &&
|
|
46
|
+
sessionState.workspaceArchive.length > 0) ||
|
|
47
|
+
parseWorkspaceArchiveObjectRef(sessionState?.workspaceArchiveRef) !== null
|
|
41
48
|
);
|
|
42
49
|
}
|
|
43
50
|
|
|
51
|
+
async function materializeArchiveObjectRef(
|
|
52
|
+
envelope: Record<string, unknown> | null,
|
|
53
|
+
objectStorage: ObjectStorage | null | undefined,
|
|
54
|
+
): Promise<Record<string, unknown> | null> {
|
|
55
|
+
if (!envelope) return null;
|
|
56
|
+
const sessionState =
|
|
57
|
+
envelope.sessionState &&
|
|
58
|
+
typeof envelope.sessionState === "object" &&
|
|
59
|
+
!Array.isArray(envelope.sessionState)
|
|
60
|
+
? (envelope.sessionState as Record<string, unknown>)
|
|
61
|
+
: null;
|
|
62
|
+
if (!sessionState) return envelope;
|
|
63
|
+
if (sessionState.workspaceArchiveRef == null) return envelope;
|
|
64
|
+
if (!parseWorkspaceArchiveObjectRef(sessionState.workspaceArchiveRef)) {
|
|
65
|
+
throw new WorkspaceArchiveIntegrityError(
|
|
66
|
+
"archive_metadata_invalid",
|
|
67
|
+
"workspace archive object reference is malformed",
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
if (!objectStorage) {
|
|
71
|
+
throw new WorkspaceArchiveIntegrityError(
|
|
72
|
+
"archive_base64_invalid",
|
|
73
|
+
"workspace archive object storage is not configured",
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
return {
|
|
77
|
+
...envelope,
|
|
78
|
+
sessionState: await inlineWorkspaceArchiveForRestore(sessionState, (key) =>
|
|
79
|
+
objectStorage.getObjectBytes(key),
|
|
80
|
+
),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
44
84
|
function legacyNativeArchiveFromEnvelope(envelope: Record<string, unknown> | null) {
|
|
45
85
|
const sessionState =
|
|
46
86
|
envelope?.sessionState && typeof envelope.sessionState === "object"
|
|
@@ -82,6 +122,7 @@ export async function establishApiSandboxSpawner(input: {
|
|
|
82
122
|
acquiredLease: LeaseSnapshot;
|
|
83
123
|
fallbackEnvelope: Record<string, unknown> | null;
|
|
84
124
|
dataPlaneUrl: string | null;
|
|
125
|
+
objectStorage?: ObjectStorage | null;
|
|
85
126
|
}): Promise<{ established: EstablishedSandboxSession; lease: LeaseSnapshot }> {
|
|
86
127
|
const fallbackArchiveEnvelope =
|
|
87
128
|
input.acquiredLease.recovery.archive.status === "none" &&
|
|
@@ -162,8 +203,10 @@ export async function establishApiSandboxSpawner(input: {
|
|
|
162
203
|
);
|
|
163
204
|
}
|
|
164
205
|
|
|
206
|
+
const hydrateEnvelope = await materializeArchiveObjectRef(spawnEnvelope, input.objectStorage);
|
|
207
|
+
|
|
165
208
|
const providerCreateStartedAt = new Date();
|
|
166
|
-
established = await establishSandboxSessionFromEnvelope(input.settings,
|
|
209
|
+
established = await establishSandboxSessionFromEnvelope(input.settings, hydrateEnvelope, {
|
|
167
210
|
sessionId: input.sessionId,
|
|
168
211
|
recovery: "create-or-restore",
|
|
169
212
|
backendOverride: input.backend as never,
|
package/src/sandbox/viewer.ts
CHANGED
|
@@ -55,6 +55,7 @@ import {
|
|
|
55
55
|
} from "@opengeni/db";
|
|
56
56
|
import { appendAndPublishEvents, type EventBus } from "@opengeni/events";
|
|
57
57
|
import { HTTPException } from "hono/http-exception";
|
|
58
|
+
import type { ObjectStorage } from "@opengeni/storage";
|
|
58
59
|
|
|
59
60
|
// The leaf — agent-loop-free. apps/api imports sandbox symbols ONLY from here
|
|
60
61
|
// (enforced by sandbox-access-import-guard.test.ts).
|
|
@@ -98,6 +99,7 @@ export type ViewerServices = {
|
|
|
98
99
|
db: Database;
|
|
99
100
|
settings: Settings;
|
|
100
101
|
bus?: EventBus;
|
|
102
|
+
objectStorage?: ObjectStorage | null;
|
|
101
103
|
/** Provider-establish dependency used by API-direct readiness and stream
|
|
102
104
|
* operations. Production uses the runtime leaf; isolated tests may supply a
|
|
103
105
|
* deterministic provider without replacing a process-global module. */
|
|
@@ -381,6 +383,7 @@ export async function attachViewer(
|
|
|
381
383
|
acquiredLease: acquired.lease,
|
|
382
384
|
fallbackEnvelope: envelope,
|
|
383
385
|
dataPlaneUrl: null,
|
|
386
|
+
...(services.objectStorage !== undefined ? { objectStorage: services.objectStorage } : {}),
|
|
384
387
|
});
|
|
385
388
|
established = result.established;
|
|
386
389
|
return {
|