@opengeni/api-router 2.13.0-canary.2 → 3.0.0-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/artifact-catalog-cursor.d.ts +27 -0
- package/dist/{chunk-K2NU4KRK.js → chunk-7IAWEYBH.js} +567 -269
- package/dist/{chunk-K2NU4KRK.js.map → chunk-7IAWEYBH.js.map} +1 -1
- package/dist/index.js +1 -1
- package/dist/routes/artifact-catalog.d.ts +6 -0
- package/dist/routes/editable-artifacts.d.ts +2 -0
- package/dist/sandbox-file-artifacts.d.ts +19 -0
- package/package.json +19 -19
- package/src/app.ts +2 -0
- package/src/artifact-catalog-cursor.ts +60 -0
- package/src/integrations/api-integrations.ts +17 -4
- package/src/mcp/knowledge-entries.ts +2 -2
- package/src/routes/artifact-catalog.ts +279 -0
- package/src/routes/editable-artifacts.ts +2 -2
- package/src/sandbox-file-artifacts.ts +9 -2
package/dist/index.js
CHANGED
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
createOpenGeniSlackBotInteractionClient,
|
|
8
8
|
startSlackInteractionPump,
|
|
9
9
|
startTemporalScheduleCleanupPump
|
|
10
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-7IAWEYBH.js";
|
|
11
11
|
|
|
12
12
|
// src/index.ts
|
|
13
13
|
import { scheduledTaskKnowledgeSource } from "@opengeni/contracts";
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { type ApiRouteDeps } from "@opengeni/core";
|
|
2
|
+
import type { Hono } from "hono";
|
|
3
|
+
import { type EditableArtifactRouteDependencies } from "./editable-artifacts.js";
|
|
4
|
+
type Dependencies = ApiRouteDeps & EditableArtifactRouteDependencies;
|
|
5
|
+
export declare function registerArtifactCatalogRoutes(app: Hono, deps: Dependencies): void;
|
|
6
|
+
export {};
|
|
@@ -42,3 +42,5 @@ export declare class EditableArtifactApplicationError extends Error {
|
|
|
42
42
|
}
|
|
43
43
|
export declare function registerEditableArtifactRoutes(app: Hono, deps: EditableArtifactRouteDependencies): void;
|
|
44
44
|
export declare function editableArtifactActorForGrant(grant: AccessGrant, rawReplicaId: string): EditableArtifactRouteActor;
|
|
45
|
+
export declare function isInvisibleListCandidate(error: unknown): boolean;
|
|
46
|
+
export declare function editableArtifactHttpError(error: unknown): unknown;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { SandboxFileArtifactReceipt, type AccessGrant, type Session } from "@opengeni/contracts";
|
|
2
2
|
import { type ApiRouteDeps, type AccessGrantAuthorization } from "@opengeni/core";
|
|
3
|
+
import { getFile } from "@opengeni/db";
|
|
3
4
|
/** Original bytes inherit the already-authorized source session's ownership. */
|
|
4
5
|
export declare function publishSandboxFileArtifact(deps: ApiRouteDeps, input: Parameters<typeof publishSandboxFileArtifactInScope>[1]): Promise<SandboxFileArtifactReceipt>;
|
|
5
6
|
declare function publishSandboxFileArtifactInScope(deps: ApiRouteDeps, input: {
|
|
@@ -12,4 +13,22 @@ declare function publishSandboxFileArtifactInScope(deps: ApiRouteDeps, input: {
|
|
|
12
13
|
export declare function sandboxArtifactRelativePath(value: string): string;
|
|
13
14
|
export declare function sandboxArtifactSafeFilename(filename: string): string;
|
|
14
15
|
export declare function sandboxFileContentType(filename: string): string;
|
|
16
|
+
export declare function sandboxArtifactIdentity(input: {
|
|
17
|
+
workspaceId: string;
|
|
18
|
+
sessionId: string;
|
|
19
|
+
path: string;
|
|
20
|
+
sha256: string;
|
|
21
|
+
}): {
|
|
22
|
+
fileId: string;
|
|
23
|
+
uploadId: string;
|
|
24
|
+
};
|
|
25
|
+
export declare function assertSandboxArtifactFile(file: NonNullable<Awaited<ReturnType<typeof getFile>>>, expected: {
|
|
26
|
+
filename: string;
|
|
27
|
+
safeFilename: string;
|
|
28
|
+
contentType: string;
|
|
29
|
+
sizeBytes: number;
|
|
30
|
+
sha256: string;
|
|
31
|
+
bucket: string;
|
|
32
|
+
objectKey: string;
|
|
33
|
+
}): void;
|
|
15
34
|
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opengeni/api-router",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.0-canary.0",
|
|
4
4
|
"description": "OpenGeni HTTP surface: the Hono adapter/router (createApp), routes, MCP HTTP transport, and HTTP access adapters over @opengeni/core. An engine-distribution surface — its runtime closure includes engine-internal packages.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -43,24 +43,24 @@
|
|
|
43
43
|
"@hono/zod-validator": "^0.7.6",
|
|
44
44
|
"@llamaindex/liteparse": "2.14.2",
|
|
45
45
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
46
|
-
"@opengeni/agent-proto": "^0.6.0-canary.
|
|
47
|
-
"@opengeni/artifact-tool": "^0.3.
|
|
48
|
-
"@opengeni/capabilities": "^0.3.3-canary.
|
|
49
|
-
"@opengeni/codemode": "^0.5.
|
|
50
|
-
"@opengeni/codex": "^0.2.
|
|
51
|
-
"@opengeni/config": "^1.2.
|
|
52
|
-
"@opengeni/contracts": "^
|
|
53
|
-
"@opengeni/core": "^
|
|
54
|
-
"@opengeni/db": "^
|
|
55
|
-
"@opengeni/documents": "^0.8.
|
|
56
|
-
"@opengeni/events": "^0.4.
|
|
57
|
-
"@opengeni/github": "^0.7.
|
|
58
|
-
"@opengeni/network": "^0.3.1-canary.
|
|
59
|
-
"@opengeni/observability": "^0.8.
|
|
60
|
-
"@opengeni/runtime": "^
|
|
61
|
-
"@opengeni/storage": "^0.2.
|
|
62
|
-
"@opengeni/tool-gateway": "^0.1.
|
|
63
|
-
"@opengeni/xai-subscription": "^0.1.4-canary.
|
|
46
|
+
"@opengeni/agent-proto": "^0.6.0-canary.3",
|
|
47
|
+
"@opengeni/artifact-tool": "^0.3.28-canary.0",
|
|
48
|
+
"@opengeni/capabilities": "^0.3.3-canary.15",
|
|
49
|
+
"@opengeni/codemode": "^0.5.8-canary.0",
|
|
50
|
+
"@opengeni/codex": "^0.2.23-canary.0",
|
|
51
|
+
"@opengeni/config": "^1.2.1-canary.0",
|
|
52
|
+
"@opengeni/contracts": "^4.0.0-canary.0",
|
|
53
|
+
"@opengeni/core": "^3.0.0-canary.0",
|
|
54
|
+
"@opengeni/db": "^5.0.0-canary.0",
|
|
55
|
+
"@opengeni/documents": "^0.8.29-canary.0",
|
|
56
|
+
"@opengeni/events": "^0.4.27-canary.0",
|
|
57
|
+
"@opengeni/github": "^0.7.12-canary.0",
|
|
58
|
+
"@opengeni/network": "^0.3.1-canary.15",
|
|
59
|
+
"@opengeni/observability": "^0.8.28-canary.0",
|
|
60
|
+
"@opengeni/runtime": "^3.0.0-canary.0",
|
|
61
|
+
"@opengeni/storage": "^0.2.129-canary.0",
|
|
62
|
+
"@opengeni/tool-gateway": "^0.1.9-canary.0",
|
|
63
|
+
"@opengeni/xai-subscription": "^0.1.4-canary.15",
|
|
64
64
|
"@temporalio/client": "^1.17.0",
|
|
65
65
|
"better-auth": "^1.6.14",
|
|
66
66
|
"hono": "^4.12.18",
|
package/src/app.ts
CHANGED
|
@@ -195,6 +195,7 @@ import { registerCompanyBrainRoutes } from "./routes/company-brain";
|
|
|
195
195
|
import { registerSlackTaskPolicyRoutes } from "./routes/slack-task-policy";
|
|
196
196
|
import { registerWorkspaceStateRoutes } from "./routes/workspace-state";
|
|
197
197
|
import { registerWorkspaceArtifactRoutes } from "./routes/workspace-artifacts";
|
|
198
|
+
import { registerArtifactCatalogRoutes } from "./routes/artifact-catalog";
|
|
198
199
|
import { registerPreferenceRegistryRoutes } from "./routes/preference-registry";
|
|
199
200
|
import { registerInsightsRoutes } from "./routes/insights";
|
|
200
201
|
import { registerTranscriptionRoutes } from "./routes/transcriptions";
|
|
@@ -1248,6 +1249,7 @@ export function createAppComposition(deps: AppDependencies): {
|
|
|
1248
1249
|
registerWorkspaceStateRoutes(app, routeDeps);
|
|
1249
1250
|
registerMemorySlackPublicationRoutes(app, routeDeps);
|
|
1250
1251
|
registerWorkspaceArtifactRoutes(app, routeDeps);
|
|
1252
|
+
registerArtifactCatalogRoutes(app, routeDeps);
|
|
1251
1253
|
registerPreferenceRegistryRoutes(app, routeDeps);
|
|
1252
1254
|
registerSocialRoutes(app, routeDeps);
|
|
1253
1255
|
registerPersonalGitHubRoutes(app, routeDeps);
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { createCipheriv, createDecipheriv, createHash, randomBytes } from "node:crypto";
|
|
2
|
+
import { ArtifactCatalogKind } from "@opengeni/contracts";
|
|
3
|
+
import { HTTPException } from "hono/http-exception";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
|
|
6
|
+
const Cursor = z
|
|
7
|
+
.object({
|
|
8
|
+
version: z.literal(1),
|
|
9
|
+
snapshotAt: z.string().datetime(),
|
|
10
|
+
expiresAt: z.number().int().positive(),
|
|
11
|
+
after: z
|
|
12
|
+
.object({
|
|
13
|
+
key: z.string().max(4096),
|
|
14
|
+
kind: ArtifactCatalogKind,
|
|
15
|
+
id: z.string().min(1).max(128),
|
|
16
|
+
})
|
|
17
|
+
.strict(),
|
|
18
|
+
})
|
|
19
|
+
.strict();
|
|
20
|
+
export type ArtifactCatalogCursor = z.infer<typeof Cursor>;
|
|
21
|
+
|
|
22
|
+
/** Encrypted as well as authenticated: a scan frontier may name a denied row.
|
|
23
|
+
* Binding includes principal, workspace, owner context, permissions and filters.
|
|
24
|
+
*/
|
|
25
|
+
export function artifactCatalogCursorCodec(secret: string, binding: string) {
|
|
26
|
+
const key = createHash("sha256").update("opengeni:artifact-catalog:v1\0").update(secret).digest();
|
|
27
|
+
const aad = createHash("sha256").update(binding).digest();
|
|
28
|
+
return {
|
|
29
|
+
encode(value: ArtifactCatalogCursor): string {
|
|
30
|
+
const iv = randomBytes(12);
|
|
31
|
+
const cipher = createCipheriv("aes-256-gcm", key, iv);
|
|
32
|
+
cipher.setAAD(aad);
|
|
33
|
+
const bytes = Buffer.concat([
|
|
34
|
+
cipher.update(JSON.stringify(Cursor.parse(value)), "utf8"),
|
|
35
|
+
cipher.final(),
|
|
36
|
+
]);
|
|
37
|
+
return Buffer.concat([iv, cipher.getAuthTag(), bytes]).toString("base64url");
|
|
38
|
+
},
|
|
39
|
+
decode(value: string): ArtifactCatalogCursor {
|
|
40
|
+
try {
|
|
41
|
+
if (value.length > 8192 || !/^[A-Za-z0-9_-]+$/.test(value)) throw new Error();
|
|
42
|
+
const bytes = Buffer.from(value, "base64url");
|
|
43
|
+
if (bytes.length < 29 || bytes.toString("base64url") !== value) throw new Error();
|
|
44
|
+
const decipher = createDecipheriv("aes-256-gcm", key, bytes.subarray(0, 12));
|
|
45
|
+
decipher.setAAD(aad);
|
|
46
|
+
decipher.setAuthTag(bytes.subarray(12, 28));
|
|
47
|
+
const parsed = Cursor.parse(
|
|
48
|
+
JSON.parse(
|
|
49
|
+
Buffer.concat([decipher.update(bytes.subarray(28)), decipher.final()]).toString("utf8"),
|
|
50
|
+
),
|
|
51
|
+
);
|
|
52
|
+
if (parsed.expiresAt <= Date.now() || Date.parse(parsed.snapshotAt) > Date.now() + 60_000)
|
|
53
|
+
throw new Error();
|
|
54
|
+
return parsed;
|
|
55
|
+
} catch {
|
|
56
|
+
throw new HTTPException(422, { message: "Invalid or expired artifact catalog cursor" });
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
}
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
googleDiscoveryToOpenApi,
|
|
11
11
|
integrationDefinitionById,
|
|
12
12
|
parseOpenApiDocument,
|
|
13
|
+
MAX_CURATED_INTEGRATION_SPEC_BYTES,
|
|
13
14
|
type IntegrationCredentialResolver,
|
|
14
15
|
type IntegrationInvocationAuthority,
|
|
15
16
|
type IntegrationTransport,
|
|
@@ -22,8 +23,6 @@ import {
|
|
|
22
23
|
} from "@opengeni/contracts";
|
|
23
24
|
import type { StoredApiIntegrationRevision } from "@opengeni/db";
|
|
24
25
|
|
|
25
|
-
const MAX_CURATED_DEFINITION_SOURCE_BYTES = 64 * 1024 * 1024;
|
|
26
|
-
|
|
27
26
|
export type ApiIntegrationConnectionDescriptor = {
|
|
28
27
|
id: string;
|
|
29
28
|
kind: "oauth2" | "api_key" | "app_install" | "delegated";
|
|
@@ -86,13 +85,18 @@ async function resolveDefinition(
|
|
|
86
85
|
const bytes = await fetchIntegrationSourceDocument(
|
|
87
86
|
input.transport,
|
|
88
87
|
definition.source.url,
|
|
89
|
-
|
|
88
|
+
MAX_CURATED_INTEGRATION_SPEC_BYTES,
|
|
90
89
|
);
|
|
91
90
|
let document: Record<string, unknown>;
|
|
92
91
|
if (definition.source.kind === "google_discovery") {
|
|
93
92
|
document = googleDiscoveryToOpenApi(JSON.parse(new TextDecoder().decode(bytes)));
|
|
94
93
|
} else {
|
|
95
|
-
document = filterOpenApiDocumentForDefinition(
|
|
94
|
+
document = filterOpenApiDocumentForDefinition(
|
|
95
|
+
parseOpenApiDocument(bytes, {
|
|
96
|
+
maxBytes: MAX_CURATED_INTEGRATION_SPEC_BYTES,
|
|
97
|
+
}),
|
|
98
|
+
definition,
|
|
99
|
+
);
|
|
96
100
|
}
|
|
97
101
|
const identity = integrationInstallationIdentity("openapi", definition.id, definition.source.url);
|
|
98
102
|
const revision = compileOpenApiRevision(document, {
|
|
@@ -100,6 +104,9 @@ async function resolveDefinition(
|
|
|
100
104
|
sourceUrl: definition.source.url,
|
|
101
105
|
baseUrl: definition.baseUrl,
|
|
102
106
|
provider: definition.provider.id,
|
|
107
|
+
...(definition.source.kind === "openapi" && definition.source.schemaMode
|
|
108
|
+
? { schemaMode: definition.source.schemaMode }
|
|
109
|
+
: {}),
|
|
103
110
|
});
|
|
104
111
|
const baseUrl = firstOpenApiServerUrl(revision);
|
|
105
112
|
const providerDomain = definition.provider.domain;
|
|
@@ -229,6 +236,12 @@ function resolvedPreview(input: {
|
|
|
229
236
|
requiredScopes: string[];
|
|
230
237
|
authScheme: Record<string, unknown>;
|
|
231
238
|
}): ResolvedApiIntegrationPreview {
|
|
239
|
+
// Include whitespace headroom for PostgreSQL jsonb::text when estimating size.
|
|
240
|
+
// The database constraint remains authoritative. Refuse during preview,
|
|
241
|
+
// before the execute-once installation claims an operation it cannot persist.
|
|
242
|
+
if (Buffer.byteLength(JSON.stringify(input.revision, null, 2), "utf8") > 4 * 1024 * 1024) {
|
|
243
|
+
throw new Error("Compiled integration exceeds the supported 4 MiB storage limit");
|
|
244
|
+
}
|
|
232
245
|
const deprecated = input.revision.tools.filter((tool) => tool.deprecated).length;
|
|
233
246
|
const warnings = [
|
|
234
247
|
...(deprecated > 0
|
|
@@ -74,7 +74,7 @@ export function registerKnowledgeEntryTools(
|
|
|
74
74
|
: state === "42501"
|
|
75
75
|
? "The instruction change is unavailable in this task's scope or Agent learning policy. Off disables authoring, not the task. Do not ask for approval to continue."
|
|
76
76
|
: state === "22023" || state === "23514"
|
|
77
|
-
? "Invalid instruction change. Read the current instruction, use append for a new rule, edit with one exact oldText match to update or remove text
|
|
77
|
+
? "Invalid instruction change. Read the current instruction, use append for a new rule, or edit with one localized exact oldText match to update or remove text. Agents cannot replace the complete instruction. The resulting instruction must stay within 600 characters."
|
|
78
78
|
: "Workspace instructions are temporarily unavailable. Retry the same operation ID only if the prior change may have succeeded."
|
|
79
79
|
: state === "40001"
|
|
80
80
|
? "This entry changed. Read its current revision and retry your correction with the current version."
|
|
@@ -259,7 +259,7 @@ export function registerKnowledgeEntryTools(
|
|
|
259
259
|
"instruction_policy_save",
|
|
260
260
|
{
|
|
261
261
|
description:
|
|
262
|
-
"Change a concise standing workspace instruction through this task's Agent learning policy. Read the current policy first and submit its exact baseline. Use editMode=append by default for a new rule; it preserves the current content and adds one blank-line separator. Use edit with oldText that occurs exactly once and newText to update or remove
|
|
262
|
+
"Change a concise standing workspace instruction through this task's Agent learning policy. Read the current policy first and submit its exact baseline. Use editMode=append by default for a new rule; it preserves the current content and adds one blank-line separator. Use edit with a localized oldText that occurs exactly once and newText to update or remove only that passage. Agents cannot replace the complete instruction; direct the user to the manual workspace-instruction editor for a whole-policy rewrite. Each supplied text and the resulting instruction are limited to 600 characters. Facts and incidents belong in knowledge_save and reusable procedures in skill_save. Review first saves an inactive revision and returns pending; continue the task without an approval question.",
|
|
263
263
|
inputSchema: AgentInstructionSaveRequest.shape,
|
|
264
264
|
},
|
|
265
265
|
(input) => run((context) => saveAgentInstruction(deps.db, context, input), "instruction"),
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
import { resolveFirstPartyDelegationSecret } from "@opengeni/config";
|
|
2
|
+
import {
|
|
3
|
+
ArtifactCatalogItem,
|
|
4
|
+
ArtifactCatalogKind,
|
|
5
|
+
ArtifactCatalogListQuery,
|
|
6
|
+
ArtifactCatalogListResponse,
|
|
7
|
+
retainedArtifactReferenceFromFile,
|
|
8
|
+
retainedGeneratedImageReferenceFromFile,
|
|
9
|
+
type AccessGrant,
|
|
10
|
+
} from "@opengeni/contracts";
|
|
11
|
+
import {
|
|
12
|
+
editableArtifactId,
|
|
13
|
+
editableArtifactScope,
|
|
14
|
+
fileOwnerContextForAccess,
|
|
15
|
+
grantHasAgentAttemptAuthority,
|
|
16
|
+
hasPermission,
|
|
17
|
+
requireAccessGrantAuthorization,
|
|
18
|
+
requireLiveAgentAttemptAuthorization,
|
|
19
|
+
requireSessionAuthorization,
|
|
20
|
+
SessionAuthorizationDeniedError,
|
|
21
|
+
SessionAuthorizationUnavailableError,
|
|
22
|
+
type ApiRouteDeps,
|
|
23
|
+
} from "@opengeni/core";
|
|
24
|
+
import {
|
|
25
|
+
getFilesForSubject,
|
|
26
|
+
getGeneratedImageArtifact,
|
|
27
|
+
getSessionAuthorityProjection,
|
|
28
|
+
listArtifactCatalogCandidates,
|
|
29
|
+
withSessionRlsActorContext,
|
|
30
|
+
type ArtifactCatalogCandidate,
|
|
31
|
+
type ArtifactCatalogPosition,
|
|
32
|
+
type SessionRlsActorContext,
|
|
33
|
+
} from "@opengeni/db";
|
|
34
|
+
import type { Hono } from "hono";
|
|
35
|
+
import { HTTPException } from "hono/http-exception";
|
|
36
|
+
import { artifactCatalogCursorCodec } from "../artifact-catalog-cursor";
|
|
37
|
+
import {
|
|
38
|
+
editableArtifactActorForGrant,
|
|
39
|
+
editableArtifactHttpError,
|
|
40
|
+
isInvisibleListCandidate,
|
|
41
|
+
type EditableArtifactRouteDependencies,
|
|
42
|
+
} from "./editable-artifacts";
|
|
43
|
+
import { fileAuthoritySubjectIdForGrant } from "./files";
|
|
44
|
+
|
|
45
|
+
type Dependencies = ApiRouteDeps & EditableArtifactRouteDependencies;
|
|
46
|
+
const editableKinds = new Set(["document", "spreadsheet", "presentation"]);
|
|
47
|
+
const CANDIDATE_BATCH = 200;
|
|
48
|
+
const MAX_BATCHES = 10;
|
|
49
|
+
const iso = (value: Date | string) => new Date(value).toISOString();
|
|
50
|
+
const position = (row: ArtifactCatalogCandidate): ArtifactCatalogPosition => ({
|
|
51
|
+
key: row.sort_key,
|
|
52
|
+
kind: row.kind,
|
|
53
|
+
id: row.id,
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
export function registerArtifactCatalogRoutes(app: Hono, deps: Dependencies): void {
|
|
57
|
+
app.get("/v1/workspaces/:workspaceId/artifact-catalog", async (context) => {
|
|
58
|
+
const workspaceId = context.req.param("workspaceId");
|
|
59
|
+
const authorization = await requireAccessGrantAuthorization(context, deps, workspaceId);
|
|
60
|
+
const grant = authorization.grant;
|
|
61
|
+
const parsed = ArtifactCatalogListQuery.safeParse(context.req.query());
|
|
62
|
+
if (!parsed.success)
|
|
63
|
+
throw new HTTPException(422, { message: "Invalid artifact catalog query" });
|
|
64
|
+
const query = parsed.data;
|
|
65
|
+
const artifactsAllowed = hasPermission(grant.permissions, "artifacts:read");
|
|
66
|
+
const filesAllowed = hasPermission(grant.permissions, "files:read");
|
|
67
|
+
if (!artifactsAllowed && !filesAllowed)
|
|
68
|
+
throw new HTTPException(403, { message: "Artifact catalog read access required" });
|
|
69
|
+
let kinds = ArtifactCatalogKind.options.filter(
|
|
70
|
+
(kind) =>
|
|
71
|
+
(kind === "image" || kind === "file" ? filesAllowed : artifactsAllowed) &&
|
|
72
|
+
(!query.kind || kind === query.kind),
|
|
73
|
+
);
|
|
74
|
+
if (!deps.editableArtifacts) {
|
|
75
|
+
if (query.kind && editableKinds.has(query.kind) && artifactsAllowed)
|
|
76
|
+
throw new HTTPException(503, { message: "Editable artifacts are unavailable" });
|
|
77
|
+
kinds = kinds.filter((kind) => !editableKinds.has(kind));
|
|
78
|
+
}
|
|
79
|
+
const includesFiles = kinds.includes("file") || kinds.includes("image");
|
|
80
|
+
const actor: SessionRlsActorContext = includesFiles
|
|
81
|
+
? await fileOwnerContextForAccess(deps, authorization, "files:read")
|
|
82
|
+
: { subjectId: grant.subjectId, privateFileOwnerSubjectId: null };
|
|
83
|
+
// Session visibility follows the frozen live initiator independently of
|
|
84
|
+
// which artifact domains were selected. Never infer it from token labels
|
|
85
|
+
// or require file ownership merely to discover Sites/editable artifacts.
|
|
86
|
+
if (grantHasAgentAttemptAuthority(grant) && actor.initiatingHumanSubjectId === undefined) {
|
|
87
|
+
const callerSessionId = grant.metadata?.["sessionId"];
|
|
88
|
+
try {
|
|
89
|
+
if (typeof callerSessionId !== "string")
|
|
90
|
+
throw new SessionAuthorizationDeniedError("caller_stale");
|
|
91
|
+
const attempt = await requireLiveAgentAttemptAuthorization(deps.db, grant, callerSessionId);
|
|
92
|
+
actor.initiatingHumanSubjectId = attempt.initiatingHumanSubjectId;
|
|
93
|
+
} catch (error) {
|
|
94
|
+
if (error instanceof SessionAuthorizationDeniedError)
|
|
95
|
+
throw new HTTPException(404, { message: "Session not found", cause: error });
|
|
96
|
+
throw error;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
const sourceDecisions = new Map<string, boolean>();
|
|
100
|
+
const canReadSession = async (sessionId: string): Promise<boolean> => {
|
|
101
|
+
if (!hasPermission(grant.permissions, "sessions:read")) return false;
|
|
102
|
+
if (sourceDecisions.has(sessionId)) return sourceDecisions.get(sessionId)!;
|
|
103
|
+
try {
|
|
104
|
+
await requireSessionAuthorization(deps, grant, {
|
|
105
|
+
sessionId,
|
|
106
|
+
operation: "session.read",
|
|
107
|
+
surface: "http",
|
|
108
|
+
});
|
|
109
|
+
// The optional-host seam can return null for both a shared standalone
|
|
110
|
+
// session and an RLS-hidden/missing row. Prove visibility independently
|
|
111
|
+
// AFTER it, so a permissive null is never itself provenance authority.
|
|
112
|
+
if (!(await getSessionAuthorityProjection(deps.db, workspaceId, sessionId))) {
|
|
113
|
+
sourceDecisions.set(sessionId, false);
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
sourceDecisions.set(sessionId, true);
|
|
117
|
+
return true;
|
|
118
|
+
} catch (error) {
|
|
119
|
+
if (error instanceof SessionAuthorizationDeniedError) {
|
|
120
|
+
sourceDecisions.set(sessionId, false);
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
if (error instanceof SessionAuthorizationUnavailableError)
|
|
124
|
+
throw new HTTPException(503, { message: "Session authorization is unavailable" });
|
|
125
|
+
throw error;
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
context.header("cache-control", "private, no-store");
|
|
129
|
+
return withSessionRlsActorContext(actor, async () => {
|
|
130
|
+
if (query.sourceSessionId && !(await canReadSession(query.sourceSessionId)))
|
|
131
|
+
throw new HTTPException(404, { message: "Session not found" });
|
|
132
|
+
const secret = resolveFirstPartyDelegationSecret(deps.settings);
|
|
133
|
+
if (!secret)
|
|
134
|
+
throw new HTTPException(503, { message: "Artifact catalog cursor signing is unavailable" });
|
|
135
|
+
const filters = { ...query, cursor: undefined };
|
|
136
|
+
const codec = artifactCatalogCursorCodec(
|
|
137
|
+
secret,
|
|
138
|
+
JSON.stringify({
|
|
139
|
+
accountId: grant.accountId,
|
|
140
|
+
workspaceId,
|
|
141
|
+
subjectId: grant.subjectId,
|
|
142
|
+
principalKind: grant.principalKind,
|
|
143
|
+
metadata: grant.metadata,
|
|
144
|
+
permissions: [...grant.permissions].sort(),
|
|
145
|
+
initiatingHumanSubjectId: actor.initiatingHumanSubjectId ?? null,
|
|
146
|
+
owner: actor.privateFileOwnerSubjectId ?? null,
|
|
147
|
+
kinds,
|
|
148
|
+
filters,
|
|
149
|
+
}),
|
|
150
|
+
);
|
|
151
|
+
const prior = query.cursor ? codec.decode(query.cursor) : undefined;
|
|
152
|
+
const snapshotAt = prior?.snapshotAt ?? new Date().toISOString();
|
|
153
|
+
const expiresAt = prior?.expiresAt ?? Date.now() + 60 * 60_000;
|
|
154
|
+
let after = prior?.after;
|
|
155
|
+
const items: ArtifactCatalogItem[] = [];
|
|
156
|
+
let lastReturned: ArtifactCatalogPosition | undefined;
|
|
157
|
+
let nextCursor: string | null = null;
|
|
158
|
+
const fileSubject = includesFiles ? await fileAuthoritySubjectIdForGrant(deps, grant) : null;
|
|
159
|
+
for (let batch = 0; batch < MAX_BATCHES; batch++) {
|
|
160
|
+
const candidates = await listArtifactCatalogCandidates(
|
|
161
|
+
deps.db,
|
|
162
|
+
{ accountId: grant.accountId, workspaceId },
|
|
163
|
+
{
|
|
164
|
+
query,
|
|
165
|
+
kinds,
|
|
166
|
+
snapshotAt,
|
|
167
|
+
...(after ? { after } : {}),
|
|
168
|
+
limit: CANDIDATE_BATCH,
|
|
169
|
+
},
|
|
170
|
+
);
|
|
171
|
+
for (const candidate of candidates) {
|
|
172
|
+
after = position(candidate);
|
|
173
|
+
const item = await projectCandidate(deps, grant, candidate, fileSubject);
|
|
174
|
+
if (
|
|
175
|
+
!item ||
|
|
176
|
+
item.status !== query.status ||
|
|
177
|
+
(query.q && !item.title.toLowerCase().includes(query.q.toLowerCase()))
|
|
178
|
+
)
|
|
179
|
+
continue;
|
|
180
|
+
if (items.length === query.limit) {
|
|
181
|
+
nextCursor = codec.encode({ version: 1, snapshotAt, expiresAt, after: lastReturned! });
|
|
182
|
+
return context.json(ArtifactCatalogListResponse.parse({ items, nextCursor }));
|
|
183
|
+
}
|
|
184
|
+
if (candidate.source_session_id && (await canReadSession(candidate.source_session_id)))
|
|
185
|
+
item.sourceSessionId = candidate.source_session_id;
|
|
186
|
+
items.push(ArtifactCatalogItem.parse(item));
|
|
187
|
+
lastReturned = after;
|
|
188
|
+
}
|
|
189
|
+
if (candidates.length < CANDIDATE_BATCH) break;
|
|
190
|
+
// Bounded scanning may produce an empty page; the encrypted frontier
|
|
191
|
+
// reveals neither denied identity nor provenance and can be continued.
|
|
192
|
+
if (batch === MAX_BATCHES - 1 && after)
|
|
193
|
+
nextCursor = codec.encode({ version: 1, snapshotAt, expiresAt, after });
|
|
194
|
+
}
|
|
195
|
+
return context.json(ArtifactCatalogListResponse.parse({ items, nextCursor }));
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
async function projectCandidate(
|
|
201
|
+
deps: Dependencies,
|
|
202
|
+
grant: AccessGrant,
|
|
203
|
+
row: ArtifactCatalogCandidate,
|
|
204
|
+
fileSubject: string | null,
|
|
205
|
+
): Promise<ArtifactCatalogItem | null> {
|
|
206
|
+
const item: ArtifactCatalogItem = {
|
|
207
|
+
id: row.id,
|
|
208
|
+
kind: row.kind,
|
|
209
|
+
title: row.title,
|
|
210
|
+
status: row.status,
|
|
211
|
+
createdAt: iso(row.created_at),
|
|
212
|
+
updatedAt: iso(row.updated_at),
|
|
213
|
+
};
|
|
214
|
+
if (row.version_id) item.versionId = row.version_id;
|
|
215
|
+
if (editableKinds.has(row.kind)) {
|
|
216
|
+
const scope = editableArtifactScope({
|
|
217
|
+
accountId: grant.accountId,
|
|
218
|
+
workspaceId: grant.workspaceId,
|
|
219
|
+
});
|
|
220
|
+
try {
|
|
221
|
+
// This is the HTTP application's read-only domain.getArtifact seam, NOT
|
|
222
|
+
// EditableArtifactAgentApplication.read (which touches session links).
|
|
223
|
+
const artifact = await deps.editableArtifacts!.readArtifact({
|
|
224
|
+
scope,
|
|
225
|
+
actor: editableArtifactActorForGrant(grant, "0000000000000001"),
|
|
226
|
+
artifactId: editableArtifactId(row.id),
|
|
227
|
+
});
|
|
228
|
+
if (
|
|
229
|
+
artifact.scope.accountId !== scope.accountId ||
|
|
230
|
+
artifact.scope.workspaceId !== scope.workspaceId ||
|
|
231
|
+
artifact.id !== row.id ||
|
|
232
|
+
artifact.modality !== row.kind
|
|
233
|
+
)
|
|
234
|
+
throw new Error("Artifact application returned mismatched catalog metadata");
|
|
235
|
+
return {
|
|
236
|
+
...item,
|
|
237
|
+
title: artifact.title,
|
|
238
|
+
status: artifact.lifecycle,
|
|
239
|
+
createdAt: artifact.createdAt,
|
|
240
|
+
updatedAt: artifact.updatedAt,
|
|
241
|
+
};
|
|
242
|
+
} catch (error) {
|
|
243
|
+
if (isInvisibleListCandidate(error)) return null;
|
|
244
|
+
throw editableArtifactHttpError(error);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
if (row.kind === "file" || row.kind === "image") {
|
|
248
|
+
const [file] = await getFilesForSubject(deps.db, {
|
|
249
|
+
accountId: grant.accountId,
|
|
250
|
+
workspaceId: grant.workspaceId,
|
|
251
|
+
subjectId: fileSubject,
|
|
252
|
+
fileIds: [row.id],
|
|
253
|
+
});
|
|
254
|
+
if (!file || file.status !== "ready") return null;
|
|
255
|
+
const generated =
|
|
256
|
+
row.origin === "generated_image"
|
|
257
|
+
? await getGeneratedImageArtifact(deps.db, grant.workspaceId, row.id)
|
|
258
|
+
: null;
|
|
259
|
+
const reference =
|
|
260
|
+
row.origin === "generated_image"
|
|
261
|
+
? generated?.status === "ready"
|
|
262
|
+
? retainedGeneratedImageReferenceFromFile({
|
|
263
|
+
...file,
|
|
264
|
+
width: generated.width,
|
|
265
|
+
height: generated.height,
|
|
266
|
+
})
|
|
267
|
+
: null
|
|
268
|
+
: retainedArtifactReferenceFromFile(file, "file");
|
|
269
|
+
if (!reference) return null;
|
|
270
|
+
return {
|
|
271
|
+
...item,
|
|
272
|
+
file: reference,
|
|
273
|
+
filename: file.filename,
|
|
274
|
+
contentType: reference.contentType,
|
|
275
|
+
sizeBytes: file.sizeBytes,
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
return item;
|
|
279
|
+
}
|
|
@@ -909,7 +909,7 @@ function normalizeArtifactIdList(value: readonly string[]): readonly string[] {
|
|
|
909
909
|
return ids;
|
|
910
910
|
}
|
|
911
911
|
|
|
912
|
-
function isInvisibleListCandidate(error: unknown): boolean {
|
|
912
|
+
export function isInvisibleListCandidate(error: unknown): boolean {
|
|
913
913
|
return (
|
|
914
914
|
(error instanceof EditableArtifactDomainError &&
|
|
915
915
|
(error.code === "forbidden" || error.code === "not_found")) ||
|
|
@@ -1050,7 +1050,7 @@ async function readBoundedJson(request: Request, maxBytes: number): Promise<unkn
|
|
|
1050
1050
|
}
|
|
1051
1051
|
}
|
|
1052
1052
|
|
|
1053
|
-
function editableArtifactHttpError(error: unknown): unknown {
|
|
1053
|
+
export function editableArtifactHttpError(error: unknown): unknown {
|
|
1054
1054
|
if (error instanceof EditableArtifactDurableExportError) {
|
|
1055
1055
|
switch (error.code) {
|
|
1056
1056
|
case "invalid_request":
|
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
prepareGeneratedWorkspaceFile,
|
|
23
23
|
getSessionAuthorityProjection,
|
|
24
24
|
requireWorkspace,
|
|
25
|
+
recordSandboxFilePublication,
|
|
25
26
|
withSessionRlsActorContext,
|
|
26
27
|
} from "@opengeni/db";
|
|
27
28
|
import { retryWhileMissing, type ObjectHead, type ObjectStorage } from "@opengeni/storage";
|
|
@@ -225,6 +226,12 @@ async function publishSandboxFileArtifactInScope(
|
|
|
225
226
|
if (!artifact) {
|
|
226
227
|
throw new HTTPException(502, { message: "published sandbox artifact is not ready" });
|
|
227
228
|
}
|
|
229
|
+
await recordSandboxFilePublication(deps.db, {
|
|
230
|
+
accountId: input.grant.accountId,
|
|
231
|
+
workspaceId: input.grant.workspaceId,
|
|
232
|
+
fileId: file.id,
|
|
233
|
+
sourceSessionId: input.session.id,
|
|
234
|
+
});
|
|
228
235
|
return SandboxFileArtifactReceipt.parse({
|
|
229
236
|
type: "sandbox_file",
|
|
230
237
|
sandboxPath: `/workspace/${path}`,
|
|
@@ -300,7 +307,7 @@ export function sandboxFileContentType(filename: string): string {
|
|
|
300
307
|
);
|
|
301
308
|
}
|
|
302
309
|
|
|
303
|
-
function sandboxArtifactIdentity(input: {
|
|
310
|
+
export function sandboxArtifactIdentity(input: {
|
|
304
311
|
workspaceId: string;
|
|
305
312
|
sessionId: string;
|
|
306
313
|
path: string;
|
|
@@ -330,7 +337,7 @@ function uuidFromDigest(digest: string, startByte: number): string {
|
|
|
330
337
|
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
331
338
|
}
|
|
332
339
|
|
|
333
|
-
function assertSandboxArtifactFile(
|
|
340
|
+
export function assertSandboxArtifactFile(
|
|
334
341
|
file: NonNullable<Awaited<ReturnType<typeof getFile>>>,
|
|
335
342
|
expected: {
|
|
336
343
|
filename: string;
|