@opengeni/api-router 2.6.4 → 2.10.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/access-grant-rls.d.ts +3 -0
- package/dist/app.d.ts +2 -1
- package/dist/app.js +3 -1
- package/dist/auth/organization-user-setup.d.ts +23 -0
- package/dist/{chunk-XTLI3CBH.js → chunk-XXH7DW34.js} +6866 -3554
- package/dist/chunk-XXH7DW34.js.map +1 -0
- package/dist/codemode.d.ts +6 -0
- package/dist/github-access.d.ts +25 -2
- package/dist/http/request-source.d.ts +14 -0
- package/dist/index.js +19 -3
- package/dist/index.js.map +1 -1
- package/dist/integrations/oauth-client.d.ts +2 -11
- package/dist/integrations/personal-github-repositories.d.ts +41 -2
- package/dist/integrations/slack-interactions.d.ts +1 -1
- package/dist/mcp/server.d.ts +158 -1
- package/dist/mcp/session-view.d.ts +56 -2
- package/dist/mcp/session-wait.d.ts +1 -1
- package/dist/mcp-oauth.d.ts +19 -0
- package/dist/model-catalog.d.ts +5 -31
- package/dist/routes/codex.d.ts +27 -2
- package/dist/routes/github.d.ts +2 -0
- package/dist/routes/organization-model-providers.d.ts +3 -0
- package/dist/routes/workspace-artifacts.d.ts +2 -1
- package/dist/work-discovery-observability.d.ts +1 -1
- package/dist/workspace-artifact-content.d.ts +28 -0
- package/dist/workspace-artifact-provenance.d.ts +7 -0
- package/dist/workspace-deletion.d.ts +6 -0
- package/dist/workspace-tool-gateway-observability.d.ts +17 -0
- package/dist/workspace-tool-gateway.d.ts +118 -0
- package/package.json +19 -18
- package/src/access-grant-rls.ts +40 -0
- package/src/app.ts +296 -53
- package/src/auth/organization-user-setup.ts +95 -5
- package/src/codemode.ts +33 -4
- package/src/github-access.ts +117 -1
- package/src/http/auth.ts +11 -0
- package/src/http/request-source.ts +37 -0
- package/src/http/sse.ts +15 -7
- package/src/index.ts +18 -2
- package/src/integrations/oauth-client.ts +168 -203
- package/src/integrations/personal-github-repositories.ts +238 -9
- package/src/integrations/slack-app-home.ts +2 -2
- package/src/integrations/slack-interactions.ts +77 -37
- package/src/mcp/company-brain-governed-writes.ts +2 -2
- package/src/mcp/company-profile-agent-admin.ts +1 -1
- package/src/mcp/remember.ts +1 -1
- package/src/mcp/server.ts +504 -133
- package/src/mcp/session-view.ts +20 -1
- package/src/mcp/session-wait.ts +3 -0
- package/src/mcp-oauth.ts +539 -0
- package/src/model-catalog.ts +45 -337
- package/src/routes/automations.ts +178 -19
- package/src/routes/codex.ts +242 -73
- package/src/routes/connections.ts +271 -16
- package/src/routes/documents.ts +67 -19
- package/src/routes/files.ts +54 -6
- package/src/routes/github.ts +116 -15
- package/src/routes/organization-memberships.ts +59 -16
- package/src/routes/organization-model-providers.ts +231 -0
- package/src/routes/packs.ts +1 -0
- package/src/routes/personal-github.ts +39 -0
- package/src/routes/pr-review.ts +72 -4
- package/src/routes/scheduled-tasks.ts +40 -13
- package/src/routes/sessions.ts +153 -65
- package/src/routes/supergrok.ts +3 -2
- package/src/routes/workspace-artifacts.ts +176 -67
- package/src/routes/workspaces.ts +405 -62
- package/src/sandbox/channel-a.ts +17 -4
- package/src/work-discovery-observability.ts +3 -3
- package/src/workspace-artifact-content.ts +163 -0
- package/src/workspace-artifact-provenance.ts +104 -0
- package/src/workspace-deletion.ts +64 -0
- package/src/workspace-tool-gateway-observability.ts +100 -0
- package/src/workspace-tool-gateway.ts +691 -0
- package/dist/chunk-XTLI3CBH.js.map +0 -1
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import {
|
|
3
|
+
WorkspaceArtifactHtml,
|
|
4
|
+
WorkspaceArtifactRequestedTools,
|
|
5
|
+
WorkspaceArtifactSourceBundle,
|
|
6
|
+
type ToolGatewayIdentity,
|
|
7
|
+
type WorkspaceArtifactSourceBundle as WorkspaceArtifactSourceBundleValue,
|
|
8
|
+
type WorkspaceArtifactVersion,
|
|
9
|
+
} from "@opengeni/contracts";
|
|
10
|
+
import type { ObjectStorageDependency } from "@opengeni/core";
|
|
11
|
+
import { retryWhileMissing } from "@opengeni/storage";
|
|
12
|
+
|
|
13
|
+
const encoder = new TextEncoder();
|
|
14
|
+
const decoder = new TextDecoder("utf-8", { fatal: true });
|
|
15
|
+
|
|
16
|
+
export type PreparedWorkspaceArtifactContent = {
|
|
17
|
+
contentKey: string;
|
|
18
|
+
contentSha256: string;
|
|
19
|
+
sizeBytes: number;
|
|
20
|
+
sourceKey: string;
|
|
21
|
+
sourceSha256: string;
|
|
22
|
+
sourceSizeBytes: number;
|
|
23
|
+
requestedTools?: ToolGatewayIdentity[];
|
|
24
|
+
persistContent: () => Promise<void>;
|
|
25
|
+
discardContent: () => Promise<void>;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export function prepareWorkspaceArtifactContent(
|
|
29
|
+
objectStorage: NonNullable<ObjectStorageDependency>,
|
|
30
|
+
workspaceId: string,
|
|
31
|
+
input: {
|
|
32
|
+
html: string;
|
|
33
|
+
source?: WorkspaceArtifactSourceBundleValue;
|
|
34
|
+
requestedTools?: ToolGatewayIdentity[];
|
|
35
|
+
},
|
|
36
|
+
): PreparedWorkspaceArtifactContent {
|
|
37
|
+
const html = WorkspaceArtifactHtml.parse(input.html);
|
|
38
|
+
const source = WorkspaceArtifactSourceBundle.parse(input.source ?? sourceBundleFromHtml(html));
|
|
39
|
+
const requestedTools =
|
|
40
|
+
input.requestedTools === undefined
|
|
41
|
+
? undefined
|
|
42
|
+
: WorkspaceArtifactRequestedTools.parse(input.requestedTools);
|
|
43
|
+
const contentBytes = encoder.encode(html);
|
|
44
|
+
const sourceBytes = encoder.encode(JSON.stringify(source));
|
|
45
|
+
const contentSha256 = sha256(contentBytes);
|
|
46
|
+
const sourceSha256 = sha256(sourceBytes);
|
|
47
|
+
const storageGroupId = crypto.randomUUID();
|
|
48
|
+
const contentKey = `workspaces/${workspaceId}/workspace-artifacts/blobs/${storageGroupId}-${contentSha256}.html`;
|
|
49
|
+
const sourceKey = `workspaces/${workspaceId}/workspace-artifacts/sources/${storageGroupId}-${sourceSha256}.json`;
|
|
50
|
+
const discardContent = async (): Promise<void> => {
|
|
51
|
+
await Promise.allSettled([
|
|
52
|
+
objectStorage.deleteObject(contentKey),
|
|
53
|
+
objectStorage.deleteObject(sourceKey),
|
|
54
|
+
]);
|
|
55
|
+
};
|
|
56
|
+
return {
|
|
57
|
+
contentKey,
|
|
58
|
+
contentSha256,
|
|
59
|
+
sizeBytes: contentBytes.byteLength,
|
|
60
|
+
sourceKey,
|
|
61
|
+
sourceSha256,
|
|
62
|
+
sourceSizeBytes: sourceBytes.byteLength,
|
|
63
|
+
...(requestedTools === undefined ? {} : { requestedTools }),
|
|
64
|
+
discardContent,
|
|
65
|
+
persistContent: async () => {
|
|
66
|
+
const writes = await Promise.allSettled([
|
|
67
|
+
objectStorage.putObject({
|
|
68
|
+
key: contentKey,
|
|
69
|
+
contentType: "text/html; charset=utf-8",
|
|
70
|
+
body: contentBytes,
|
|
71
|
+
sha256: contentSha256,
|
|
72
|
+
}),
|
|
73
|
+
objectStorage.putObject({
|
|
74
|
+
key: sourceKey,
|
|
75
|
+
contentType: "application/json; charset=utf-8",
|
|
76
|
+
body: sourceBytes,
|
|
77
|
+
sha256: sourceSha256,
|
|
78
|
+
}),
|
|
79
|
+
]);
|
|
80
|
+
const failed = writes.find((write) => write.status === "rejected");
|
|
81
|
+
if (!failed) return;
|
|
82
|
+
await Promise.allSettled(
|
|
83
|
+
writes.flatMap((write, index) =>
|
|
84
|
+
write.status === "fulfilled"
|
|
85
|
+
? [objectStorage.deleteObject(index === 0 ? contentKey : sourceKey)]
|
|
86
|
+
: [],
|
|
87
|
+
),
|
|
88
|
+
);
|
|
89
|
+
throw failed.reason;
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export async function readWorkspaceArtifactContent(
|
|
95
|
+
objectStorage: NonNullable<ObjectStorageDependency>,
|
|
96
|
+
input: {
|
|
97
|
+
contentKey: string;
|
|
98
|
+
sourceKey: string | null;
|
|
99
|
+
version: WorkspaceArtifactVersion;
|
|
100
|
+
},
|
|
101
|
+
): Promise<{
|
|
102
|
+
html: string;
|
|
103
|
+
source: WorkspaceArtifactSourceBundleValue;
|
|
104
|
+
requestedTools: ToolGatewayIdentity[];
|
|
105
|
+
}> {
|
|
106
|
+
const [contentObject, sourceObject] = await Promise.all([
|
|
107
|
+
retryWhileMissing(async () => await objectStorage.getObjectBytes(input.contentKey)),
|
|
108
|
+
input.sourceKey
|
|
109
|
+
? retryWhileMissing(async () => await objectStorage.getObjectBytes(input.sourceKey!))
|
|
110
|
+
: Promise.resolve(null),
|
|
111
|
+
]);
|
|
112
|
+
if (!contentObject) throw new Error("Artifact content is unavailable");
|
|
113
|
+
if (
|
|
114
|
+
sha256(contentObject.bytes) !== input.version.contentSha256 ||
|
|
115
|
+
contentObject.bytes.byteLength !== input.version.sizeBytes
|
|
116
|
+
) {
|
|
117
|
+
throw new Error("Artifact content failed integrity verification");
|
|
118
|
+
}
|
|
119
|
+
const html = decode(contentObject.bytes, "Artifact content is not valid UTF-8");
|
|
120
|
+
let source = sourceBundleFromHtml(html);
|
|
121
|
+
if (input.sourceKey) {
|
|
122
|
+
if (!sourceObject) throw new Error("Artifact source is unavailable");
|
|
123
|
+
if (
|
|
124
|
+
!input.version.sourceSha256 ||
|
|
125
|
+
input.version.sourceSizeBytes === null ||
|
|
126
|
+
sha256(sourceObject.bytes) !== input.version.sourceSha256 ||
|
|
127
|
+
sourceObject.bytes.byteLength !== input.version.sourceSizeBytes
|
|
128
|
+
) {
|
|
129
|
+
throw new Error("Artifact source failed integrity verification");
|
|
130
|
+
}
|
|
131
|
+
try {
|
|
132
|
+
source = WorkspaceArtifactSourceBundle.parse(
|
|
133
|
+
JSON.parse(decode(sourceObject.bytes, "Artifact source is not valid UTF-8")),
|
|
134
|
+
);
|
|
135
|
+
} catch (error) {
|
|
136
|
+
if (error instanceof Error && error.message === "Artifact source is not valid UTF-8") {
|
|
137
|
+
throw error;
|
|
138
|
+
}
|
|
139
|
+
throw new Error("Artifact source is invalid", { cause: error });
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return {
|
|
143
|
+
html,
|
|
144
|
+
source,
|
|
145
|
+
requestedTools: WorkspaceArtifactRequestedTools.parse(input.version.requestedTools),
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function sourceBundleFromHtml(html: string): WorkspaceArtifactSourceBundleValue {
|
|
150
|
+
return { entrypoint: "index.html", files: [{ path: "index.html", content: html }] };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function sha256(bytes: Uint8Array): string {
|
|
154
|
+
return createHash("sha256").update(bytes).digest("hex");
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function decode(bytes: Uint8Array, message: string): string {
|
|
158
|
+
try {
|
|
159
|
+
return decoder.decode(bytes);
|
|
160
|
+
} catch (error) {
|
|
161
|
+
throw new Error(message, { cause: error });
|
|
162
|
+
}
|
|
163
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
WorkspaceArtifactDetailResponse,
|
|
3
|
+
WorkspaceArtifactListResponse,
|
|
4
|
+
WorkspaceArtifactMutationResponse,
|
|
5
|
+
WorkspaceArtifactVersion,
|
|
6
|
+
} from "@opengeni/contracts";
|
|
7
|
+
|
|
8
|
+
type ArtifactProvenance = {
|
|
9
|
+
sourceSessionId: string | null;
|
|
10
|
+
sourceTurnId: string | null;
|
|
11
|
+
sourceAttemptId: string | null;
|
|
12
|
+
sourceExecutionGeneration: number | null;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
type CanReadSession = (sessionId: string) => Promise<boolean>;
|
|
16
|
+
|
|
17
|
+
function redactProvenance<Value extends ArtifactProvenance>(value: Value): Value {
|
|
18
|
+
return {
|
|
19
|
+
...value,
|
|
20
|
+
sourceSessionId: null,
|
|
21
|
+
sourceTurnId: null,
|
|
22
|
+
sourceAttemptId: null,
|
|
23
|
+
sourceExecutionGeneration: null,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function artifactProvenanceProjector(canReadSession: CanReadSession) {
|
|
28
|
+
const decisions = new Map<string, Promise<boolean>>();
|
|
29
|
+
const canRead = (sessionId: string): Promise<boolean> => {
|
|
30
|
+
const existing = decisions.get(sessionId);
|
|
31
|
+
if (existing) return existing;
|
|
32
|
+
const decision = canReadSession(sessionId);
|
|
33
|
+
decisions.set(sessionId, decision);
|
|
34
|
+
return decision;
|
|
35
|
+
};
|
|
36
|
+
return async <Value extends ArtifactProvenance>(value: Value): Promise<Value> => {
|
|
37
|
+
if (!value.sourceSessionId || (await canRead(value.sourceSessionId))) return value;
|
|
38
|
+
return redactProvenance(value);
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function redactWorkspaceArtifactListProvenance(
|
|
43
|
+
response: WorkspaceArtifactListResponse,
|
|
44
|
+
): WorkspaceArtifactListResponse {
|
|
45
|
+
return {
|
|
46
|
+
...response,
|
|
47
|
+
artifacts: response.artifacts.map((artifact) => ({
|
|
48
|
+
...artifact,
|
|
49
|
+
currentVersion: artifact.currentVersion ? redactProvenance(artifact.currentVersion) : null,
|
|
50
|
+
})),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export async function projectWorkspaceArtifactVersionProvenance(
|
|
55
|
+
version: WorkspaceArtifactVersion,
|
|
56
|
+
canReadSession: CanReadSession,
|
|
57
|
+
): Promise<WorkspaceArtifactVersion> {
|
|
58
|
+
return await artifactProvenanceProjector(canReadSession)(version);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export async function projectWorkspaceArtifactDetailProvenance(
|
|
62
|
+
detail: WorkspaceArtifactDetailResponse,
|
|
63
|
+
canReadSession: CanReadSession,
|
|
64
|
+
): Promise<WorkspaceArtifactDetailResponse> {
|
|
65
|
+
const project = artifactProvenanceProjector(canReadSession);
|
|
66
|
+
const [currentVersion, versions, events] = await Promise.all([
|
|
67
|
+
detail.artifact.currentVersion ? project(detail.artifact.currentVersion) : null,
|
|
68
|
+
Promise.all(detail.versions.map(project)),
|
|
69
|
+
Promise.all(detail.events.map(project)),
|
|
70
|
+
]);
|
|
71
|
+
return {
|
|
72
|
+
...detail,
|
|
73
|
+
artifact: { ...detail.artifact, currentVersion },
|
|
74
|
+
versions,
|
|
75
|
+
events,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export async function projectWorkspaceArtifactMutationProvenance(
|
|
80
|
+
response: WorkspaceArtifactMutationResponse,
|
|
81
|
+
canReadSession: CanReadSession,
|
|
82
|
+
): Promise<WorkspaceArtifactMutationResponse> {
|
|
83
|
+
// The mutation is already durable before this response projection runs. A
|
|
84
|
+
// transient source-session lookup must fail closed to redaction rather than
|
|
85
|
+
// turn a committed, idempotent mutation into an ambiguous HTTP/tool error.
|
|
86
|
+
const project = artifactProvenanceProjector(async (sessionId) => {
|
|
87
|
+
try {
|
|
88
|
+
return await canReadSession(sessionId);
|
|
89
|
+
} catch {
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
const [currentVersion, version, event] = await Promise.all([
|
|
94
|
+
response.artifact.currentVersion ? project(response.artifact.currentVersion) : null,
|
|
95
|
+
project(response.version),
|
|
96
|
+
project(response.event),
|
|
97
|
+
]);
|
|
98
|
+
return {
|
|
99
|
+
...response,
|
|
100
|
+
artifact: { ...response.artifact, currentVersion },
|
|
101
|
+
version,
|
|
102
|
+
event,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { deleteWorkspaceIfQuiescent, nestedPostgresSqlState } from "@opengeni/db";
|
|
2
|
+
import type { ApiRouteDeps } from "@opengeni/core";
|
|
3
|
+
import { HTTPException } from "hono/http-exception";
|
|
4
|
+
|
|
5
|
+
import { processTemporalScheduleCleanupClaims } from "./temporal-schedule-cleanup";
|
|
6
|
+
import { workspaceDeleteObserver } from "./workspace-delete-observability";
|
|
7
|
+
|
|
8
|
+
export async function deleteWorkspaceForRequest(
|
|
9
|
+
deps: ApiRouteDeps,
|
|
10
|
+
input: {
|
|
11
|
+
accountId: string;
|
|
12
|
+
workspaceId: string;
|
|
13
|
+
organizationAdministratorSubjectId?: string;
|
|
14
|
+
},
|
|
15
|
+
): Promise<void> {
|
|
16
|
+
const observer = workspaceDeleteObserver(deps.observability, input);
|
|
17
|
+
let deleted;
|
|
18
|
+
try {
|
|
19
|
+
deleted = await deleteWorkspaceIfQuiescent(deps.db, {
|
|
20
|
+
...input,
|
|
21
|
+
...(observer ? { observer } : {}),
|
|
22
|
+
});
|
|
23
|
+
} catch (error) {
|
|
24
|
+
const state = nestedPostgresSqlState(error);
|
|
25
|
+
if (state === "42501") {
|
|
26
|
+
throw new HTTPException(403, { message: "organization administration is not authorized" });
|
|
27
|
+
}
|
|
28
|
+
if (state === "P0002") {
|
|
29
|
+
throw new HTTPException(404, { message: "workspace not found" });
|
|
30
|
+
}
|
|
31
|
+
throw error;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (deleted.status === "not_found") {
|
|
35
|
+
throw new HTTPException(404, { message: "workspace not found" });
|
|
36
|
+
}
|
|
37
|
+
const conflicts = {
|
|
38
|
+
only_workspace: "cannot delete the account's only workspace",
|
|
39
|
+
active_sessions: "stop the workspace's running sessions before deleting it",
|
|
40
|
+
active_video_generations:
|
|
41
|
+
"wait for the workspace's active video generations to finish before deleting it",
|
|
42
|
+
active_background_commands:
|
|
43
|
+
"pause or cancel the workspace's background commands before deleting it",
|
|
44
|
+
live_sandboxes:
|
|
45
|
+
"wait for the workspace's active sandboxes to finish draining before deleting it",
|
|
46
|
+
} as const;
|
|
47
|
+
if (deleted.status in conflicts) {
|
|
48
|
+
throw new HTTPException(409, { message: conflicts[deleted.status as keyof typeof conflicts] });
|
|
49
|
+
}
|
|
50
|
+
if (deleted.status !== "deleted") {
|
|
51
|
+
throw new Error(`Unhandled workspace deletion outcome: ${deleted.status}`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
await processTemporalScheduleCleanupClaims(
|
|
55
|
+
{
|
|
56
|
+
db: deps.db,
|
|
57
|
+
deleteSchedule: async (temporalScheduleId) => {
|
|
58
|
+
await deps.workflowClient.deleteScheduledTaskSchedule({ temporalScheduleId });
|
|
59
|
+
},
|
|
60
|
+
...(deps.observability ? { observability: deps.observability } : {}),
|
|
61
|
+
},
|
|
62
|
+
deleted.temporalScheduleCleanups,
|
|
63
|
+
);
|
|
64
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import type { ToolGatewayCatalogEntry } from "@opengeni/contracts";
|
|
2
|
+
import type { Observability } from "@opengeni/observability";
|
|
3
|
+
|
|
4
|
+
export type WorkspaceToolGatewayAdapter = "http" | "mcp";
|
|
5
|
+
export type WorkspaceToolGatewayOperation = "approval" | "call";
|
|
6
|
+
export type WorkspaceToolGatewayOutcome =
|
|
7
|
+
| "ok"
|
|
8
|
+
| "tool_error"
|
|
9
|
+
| "approval_required"
|
|
10
|
+
| "catalog_stale"
|
|
11
|
+
| "invalid_input"
|
|
12
|
+
| "not_found"
|
|
13
|
+
| "rate_limited"
|
|
14
|
+
| "failed";
|
|
15
|
+
|
|
16
|
+
const DURATION_BUCKETS = [0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10, 30, 60];
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Low-cardinality gateway telemetry shared by HTTP/SDK/Site and aggregate MCP
|
|
20
|
+
* adapters. Tool names, workspace/subject ids, arguments, results, credentials,
|
|
21
|
+
* and approval tokens are excluded by construction.
|
|
22
|
+
*/
|
|
23
|
+
export function startWorkspaceToolGatewayObservation(
|
|
24
|
+
observability: Observability | null | undefined,
|
|
25
|
+
input: {
|
|
26
|
+
adapter: WorkspaceToolGatewayAdapter;
|
|
27
|
+
operation: WorkspaceToolGatewayOperation;
|
|
28
|
+
source: ToolGatewayCatalogEntry["source"] | "aggregate";
|
|
29
|
+
},
|
|
30
|
+
): { end: (outcome: WorkspaceToolGatewayOutcome) => void } {
|
|
31
|
+
const startedAt = performance.now();
|
|
32
|
+
let span: ReturnType<Observability["startSpan"]> | undefined;
|
|
33
|
+
try {
|
|
34
|
+
span = observability?.startSpan("opengeni.tool_gateway.operation", {
|
|
35
|
+
surface: "workspace_tool_gateway",
|
|
36
|
+
op: `${input.adapter}.${input.operation}`,
|
|
37
|
+
provider: input.source,
|
|
38
|
+
});
|
|
39
|
+
} catch {
|
|
40
|
+
// Telemetry must never change gateway execution truth.
|
|
41
|
+
}
|
|
42
|
+
let ended = false;
|
|
43
|
+
return {
|
|
44
|
+
end: (outcome) => {
|
|
45
|
+
if (ended) return;
|
|
46
|
+
ended = true;
|
|
47
|
+
const durationMs = Math.max(0, performance.now() - startedAt);
|
|
48
|
+
try {
|
|
49
|
+
observability?.incrementCounter({
|
|
50
|
+
name: "opengeni_tool_gateway_operations_total",
|
|
51
|
+
help: "Workspace tool gateway operations by bounded adapter, operation, source, and outcome.",
|
|
52
|
+
labels: {
|
|
53
|
+
adapter: input.adapter,
|
|
54
|
+
operation: input.operation,
|
|
55
|
+
source: input.source,
|
|
56
|
+
outcome,
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
observability?.observeHistogram({
|
|
60
|
+
name: "opengeni_tool_gateway_operation_duration_seconds",
|
|
61
|
+
help: "Workspace tool gateway operation duration in seconds.",
|
|
62
|
+
labels: {
|
|
63
|
+
adapter: input.adapter,
|
|
64
|
+
operation: input.operation,
|
|
65
|
+
source: input.source,
|
|
66
|
+
},
|
|
67
|
+
buckets: DURATION_BUCKETS,
|
|
68
|
+
value: durationMs / 1_000,
|
|
69
|
+
});
|
|
70
|
+
observability?.info("Workspace tool gateway operation completed", {
|
|
71
|
+
surface: "workspace_tool_gateway",
|
|
72
|
+
op: `${input.adapter}.${input.operation}`,
|
|
73
|
+
provider: input.source,
|
|
74
|
+
outcome,
|
|
75
|
+
durationMs: Math.round(durationMs),
|
|
76
|
+
});
|
|
77
|
+
} catch {
|
|
78
|
+
try {
|
|
79
|
+
observability?.incrementCounter({
|
|
80
|
+
name: "opengeni_observability_observer_errors_total",
|
|
81
|
+
help: "Observability observer failures isolated from product execution.",
|
|
82
|
+
labels: { observer: "workspace_tool_gateway" },
|
|
83
|
+
});
|
|
84
|
+
} catch {
|
|
85
|
+
// Telemetry must never change gateway execution truth.
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
try {
|
|
89
|
+
span?.end({
|
|
90
|
+
attributes: {
|
|
91
|
+
outcome,
|
|
92
|
+
"opengeni.duration_ms": Math.round(durationMs),
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
} catch {
|
|
96
|
+
// Telemetry must never change gateway execution truth.
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
}
|