@opengeni/api-router 2.5.0 → 2.6.4
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/auth/managed-auth-attempt-context.d.ts +5 -1
- package/dist/auth/managed-auth.d.ts +24 -1
- package/dist/{chunk-QESX7HDK.js → chunk-XTLI3CBH.js} +2236 -387
- package/dist/chunk-XTLI3CBH.js.map +1 -0
- package/dist/http/sse.d.ts +9 -0
- package/dist/index.js +104 -4
- package/dist/index.js.map +1 -1
- package/dist/interaction-metrics.d.ts +2 -0
- package/dist/mcp/company-brain-governed-writes.d.ts +1 -1
- package/dist/mcp/company-profile-agent-admin.d.ts +6 -6
- package/dist/mcp/remember.d.ts +2 -2
- package/dist/mcp/server.d.ts +1 -0
- package/dist/mcp/session-view.d.ts +1 -0
- package/dist/mcp/session-wait.d.ts +19 -0
- package/dist/routes/api-keys.d.ts +2 -0
- package/dist/routes/browser-sessions.d.ts +1 -0
- package/dist/routes/computer-sessions.d.ts +12 -0
- package/dist/routes/managed-auth-session-sets.d.ts +7 -0
- package/dist/routes/workspaces.d.ts +1 -1
- package/dist/sandbox/metrics-ingestion.d.ts +16 -0
- package/dist/workspace-delete-observability.d.ts +10 -0
- package/package.json +15 -15
- package/src/app.ts +172 -20
- package/src/auth/managed-auth-attempt-context.ts +40 -3
- package/src/auth/managed-auth-session-adapter.ts +1 -0
- package/src/auth/managed-auth.ts +164 -4
- package/src/http/sse.ts +279 -45
- package/src/integrations/oauth-client.ts +8 -1
- package/src/integrations/provider-oauth.ts +12 -2
- package/src/interaction-metrics.ts +30 -0
- package/src/mcp/company-brain-governed-writes.ts +38 -23
- package/src/mcp/company-profile-agent-admin.ts +7 -7
- package/src/mcp/remember.ts +19 -8
- package/src/mcp/server.ts +318 -26
- package/src/mcp/session-wait.ts +56 -8
- package/src/routes/api-integrations.ts +2 -2
- package/src/routes/api-keys.ts +149 -7
- package/src/routes/browser-sessions.ts +10 -2
- package/src/routes/capabilities.ts +3 -3
- package/src/routes/codex.ts +483 -74
- package/src/routes/company-profile.ts +64 -0
- package/src/routes/computer-sessions.ts +103 -1
- package/src/routes/integration-facets.ts +8 -5
- package/src/routes/interaction-resources.ts +7 -1
- package/src/routes/managed-auth-session-sets.ts +199 -2
- package/src/routes/organization-memberships.ts +28 -8
- package/src/routes/packs.ts +5 -5
- package/src/routes/plugins.ts +2 -2
- package/src/routes/scheduled-tasks.ts +9 -0
- package/src/routes/sessions.ts +3 -6
- package/src/routes/skills.ts +3 -3
- package/src/routes/workspaces.ts +293 -54
- package/src/sandbox/channel-a.ts +10 -4
- package/src/sandbox/machines.ts +13 -6
- package/src/sandbox/metrics-ingestion.ts +157 -3
- package/src/sandbox/viewer.ts +20 -1
- package/src/workspace-delete-observability.ts +75 -0
- package/dist/chunk-QESX7HDK.js.map +0 -1
|
@@ -28,7 +28,11 @@
|
|
|
28
28
|
// Both consumers are BEST-EFFORT and fail-soft: a decode/DB error for one message
|
|
29
29
|
// is logged + swallowed (the bus subscription already swallows handler throws) so
|
|
30
30
|
// a metrics blip / a display-refresh write failure never tears down the consumer,
|
|
31
|
-
// back-pressures the agent, or breaks its connect.
|
|
31
|
+
// back-pressures the agent, or breaks its connect. Event ingestion drains the NATS
|
|
32
|
+
// subscription immediately into exact-subject queues: different runner connections
|
|
33
|
+
// progress concurrently, one runner's ordering is preserved, and consecutive queued
|
|
34
|
+
// heartbeats collapse latest-wins. A delayed heartbeat backlog therefore cannot keep
|
|
35
|
+
// renewing a dead runner's short connection lease one stale message at a time.
|
|
32
36
|
|
|
33
37
|
import {
|
|
34
38
|
clearEnrollmentWentOffline,
|
|
@@ -74,6 +78,141 @@ export const AGENT_EVENTS_SUBJECT = "agent.*.*.connection.*.events";
|
|
|
74
78
|
/** The wildcard subject the agent publishes its connect Hello on. */
|
|
75
79
|
export const AGENT_HELLO_SUBJECT = "agent.*.*.connection.*.hello";
|
|
76
80
|
|
|
81
|
+
type QueuedAgentEvent = {
|
|
82
|
+
payload: Uint8Array;
|
|
83
|
+
subject: string;
|
|
84
|
+
heartbeat: boolean;
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
type AgentEventQueueState = {
|
|
88
|
+
scheduled: boolean;
|
|
89
|
+
pending: QueuedAgentEvent[];
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
type ScheduledAgentEventQueue = {
|
|
93
|
+
subject: string;
|
|
94
|
+
state: AgentEventQueueState;
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
export type AgentEventIngestionScheduler = {
|
|
98
|
+
enqueue: (payload: Uint8Array, subject: string) => void;
|
|
99
|
+
whenIdle: () => Promise<void>;
|
|
100
|
+
close: () => void;
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
function payloadIsHeartbeat(payload: Uint8Array): boolean {
|
|
104
|
+
try {
|
|
105
|
+
return AgentEvent.decode(payload).event?.$case === "heartbeat";
|
|
106
|
+
} catch {
|
|
107
|
+
// Preserve the normal decoder/logging path for malformed events. They are
|
|
108
|
+
// barriers rather than heartbeat-coalescing candidates.
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Drain agent events without making every Connected Machine wait behind one
|
|
115
|
+
* deployment-wide serial DB queue. Each exact process subject retains FIFO order;
|
|
116
|
+
* separate subjects drain concurrently. Consecutive pending heartbeats for one
|
|
117
|
+
* subject are latest-wins because they are point-in-time liveness/metrics samples,
|
|
118
|
+
* while GoingOffline and update-progress events remain ordered barriers.
|
|
119
|
+
*/
|
|
120
|
+
export function createAgentEventIngestionScheduler(
|
|
121
|
+
handler: (payload: Uint8Array, subject: string) => void | Promise<void>,
|
|
122
|
+
options: { maxConcurrentSubjects?: number; onHeartbeatCoalesced?: () => void } = {},
|
|
123
|
+
): AgentEventIngestionScheduler {
|
|
124
|
+
const maxConcurrentSubjects = options.maxConcurrentSubjects ?? 32;
|
|
125
|
+
if (!Number.isSafeInteger(maxConcurrentSubjects) || maxConcurrentSubjects <= 0) {
|
|
126
|
+
throw new RangeError("maxConcurrentSubjects must be a positive safe integer");
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const states = new Map<string, AgentEventQueueState>();
|
|
130
|
+
const readySubjects: ScheduledAgentEventQueue[] = [];
|
|
131
|
+
const idleWaiters = new Set<() => void>();
|
|
132
|
+
let accepting = true;
|
|
133
|
+
let activeSubjects = 0;
|
|
134
|
+
|
|
135
|
+
const resolveIdleWaiters = (): void => {
|
|
136
|
+
if (states.size !== 0) return;
|
|
137
|
+
for (const resolve of idleWaiters) resolve();
|
|
138
|
+
idleWaiters.clear();
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
async function drain(subject: string, state: AgentEventQueueState): Promise<void> {
|
|
142
|
+
while (true) {
|
|
143
|
+
const event = state.pending.shift();
|
|
144
|
+
if (!event) break;
|
|
145
|
+
try {
|
|
146
|
+
await handler(event.payload, event.subject);
|
|
147
|
+
} catch {
|
|
148
|
+
// Agent-event ingestion is best-effort. One bad event must not strand the
|
|
149
|
+
// exact-subject queue or prevent later heartbeats from restoring liveness.
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
activeSubjects -= 1;
|
|
154
|
+
state.scheduled = false;
|
|
155
|
+
if (state.pending.length > 0) {
|
|
156
|
+
schedule(subject, state);
|
|
157
|
+
} else {
|
|
158
|
+
if (states.get(subject) === state) states.delete(subject);
|
|
159
|
+
resolveIdleWaiters();
|
|
160
|
+
}
|
|
161
|
+
pump();
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function pump(): void {
|
|
165
|
+
while (activeSubjects < maxConcurrentSubjects) {
|
|
166
|
+
const next = readySubjects.shift();
|
|
167
|
+
if (!next) return;
|
|
168
|
+
if (states.get(next.subject) !== next.state || !next.state.scheduled) continue;
|
|
169
|
+
activeSubjects += 1;
|
|
170
|
+
void drain(next.subject, next.state);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function schedule(subject: string, state: AgentEventQueueState): void {
|
|
175
|
+
if (state.scheduled) return;
|
|
176
|
+
state.scheduled = true;
|
|
177
|
+
readySubjects.push({ subject, state });
|
|
178
|
+
pump();
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return {
|
|
182
|
+
enqueue(payload, subject) {
|
|
183
|
+
if (!accepting) return;
|
|
184
|
+
const heartbeat = payloadIsHeartbeat(payload);
|
|
185
|
+
let state = states.get(subject);
|
|
186
|
+
if (!state) {
|
|
187
|
+
state = { scheduled: false, pending: [] };
|
|
188
|
+
states.set(subject, state);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const queued = { payload, subject, heartbeat };
|
|
192
|
+
const lastIndex = state.pending.length - 1;
|
|
193
|
+
if (heartbeat && lastIndex >= 0 && state.pending[lastIndex]!.heartbeat) {
|
|
194
|
+
state.pending[lastIndex] = queued;
|
|
195
|
+
options.onHeartbeatCoalesced?.();
|
|
196
|
+
} else {
|
|
197
|
+
state.pending.push(queued);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
schedule(subject, state);
|
|
201
|
+
},
|
|
202
|
+
async whenIdle() {
|
|
203
|
+
if (states.size === 0) return;
|
|
204
|
+
await new Promise<void>((resolve) => idleWaiters.add(resolve));
|
|
205
|
+
},
|
|
206
|
+
close() {
|
|
207
|
+
accepting = false;
|
|
208
|
+
for (const state of states.values()) {
|
|
209
|
+
state.pending.length = 0;
|
|
210
|
+
}
|
|
211
|
+
resolveIdleWaiters();
|
|
212
|
+
},
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
77
216
|
/**
|
|
78
217
|
* Parse `agent.<ws>.<id>.connection.<instance>.<tail>` into its exact authority,
|
|
79
218
|
* expected tail token. Returns null for a subject that does not match the shape
|
|
@@ -521,9 +660,24 @@ export function startMetricsIngestion(deps: {
|
|
|
521
660
|
bus: EventBus;
|
|
522
661
|
observability?: Observability;
|
|
523
662
|
}): () => void {
|
|
524
|
-
|
|
525
|
-
|
|
663
|
+
const scheduler = createAgentEventIngestionScheduler(
|
|
664
|
+
(payload, subject) =>
|
|
665
|
+
handleAgentEventPayload(deps.db, deps.observability, payload, subject, deps.bus),
|
|
666
|
+
{
|
|
667
|
+
onHeartbeatCoalesced: () =>
|
|
668
|
+
deps.observability?.incrementCounter({
|
|
669
|
+
name: "opengeni_machine_heartbeat_coalesced_total",
|
|
670
|
+
help: "Connected Machine heartbeats collapsed while an exact runner event queue was busy.",
|
|
671
|
+
}),
|
|
672
|
+
},
|
|
526
673
|
);
|
|
674
|
+
const unsubscribe = deps.bus.subscribeAgentEvents(AGENT_EVENTS_SUBJECT, (payload, subject) => {
|
|
675
|
+
scheduler.enqueue(payload, subject);
|
|
676
|
+
});
|
|
677
|
+
return () => {
|
|
678
|
+
unsubscribe();
|
|
679
|
+
scheduler.close();
|
|
680
|
+
};
|
|
527
681
|
}
|
|
528
682
|
|
|
529
683
|
// ── Connect-Hello display refresh ─────────────────────────────────────────────
|
package/src/sandbox/viewer.ts
CHANGED
|
@@ -81,6 +81,8 @@ import {
|
|
|
81
81
|
type NatsRequestConnection,
|
|
82
82
|
} from "@opengeni/runtime/sandbox";
|
|
83
83
|
import {
|
|
84
|
+
managedSessionGroupBackend,
|
|
85
|
+
managedSessionGroupOs,
|
|
84
86
|
providerSettingsForSessionSandboxRuntime,
|
|
85
87
|
relayConfigFromSettings,
|
|
86
88
|
resolveSessionSandboxRuntime,
|
|
@@ -246,7 +248,24 @@ export async function attachViewer(
|
|
|
246
248
|
},
|
|
247
249
|
): Promise<ViewerAttachResult> {
|
|
248
250
|
const { db, settings } = services;
|
|
249
|
-
const { accountId, workspaceId
|
|
251
|
+
const { accountId, workspaceId } = input;
|
|
252
|
+
const groupBackend = managedSessionGroupBackend(
|
|
253
|
+
settings.sandboxBackend,
|
|
254
|
+
input.session.sandboxBackend,
|
|
255
|
+
);
|
|
256
|
+
if (!groupBackend) {
|
|
257
|
+
throw new HTTPException(409, {
|
|
258
|
+
message: "session has no managed sandbox group to attach",
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
const session =
|
|
262
|
+
groupBackend === input.session.sandboxBackend
|
|
263
|
+
? input.session
|
|
264
|
+
: {
|
|
265
|
+
...input.session,
|
|
266
|
+
sandboxBackend: groupBackend,
|
|
267
|
+
sandboxOs: managedSessionGroupOs(input.session.sandboxBackend, input.session.sandboxOs),
|
|
268
|
+
};
|
|
250
269
|
const viewerId = input.viewerId ?? crypto.randomUUID();
|
|
251
270
|
const attachSubjectId = claimableSubjectId(input.viewerSubjectId ?? null);
|
|
252
271
|
const attachAuthorityEpoch = attachSubjectId
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import type { WorkspaceDeleteObservation, WorkspaceDeleteObserver } from "@opengeni/db";
|
|
2
|
+
import type { Observability } from "@opengeni/observability";
|
|
3
|
+
|
|
4
|
+
const DURATION_BUCKETS = [
|
|
5
|
+
0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10, 30, 60, 120, 300, 600, 1_200, 2_400,
|
|
6
|
+
3_600,
|
|
7
|
+
];
|
|
8
|
+
const INVENTORY_BUCKETS = [0, 1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1_000, 5_000, 10_000];
|
|
9
|
+
|
|
10
|
+
function recordWorkspaceDeleteObservation(
|
|
11
|
+
observability: Observability,
|
|
12
|
+
identity: { accountId: string; workspaceId: string },
|
|
13
|
+
observation: WorkspaceDeleteObservation,
|
|
14
|
+
): void {
|
|
15
|
+
const labels = { phase: observation.phase, outcome: observation.outcome };
|
|
16
|
+
observability.observeHistogram({
|
|
17
|
+
name: "opengeni_workspace_delete_phase_seconds",
|
|
18
|
+
help: "Workspace deletion transaction and bounded internal phase duration in seconds.",
|
|
19
|
+
labels,
|
|
20
|
+
buckets: DURATION_BUCKETS,
|
|
21
|
+
value: Math.max(0, observation.durationSeconds),
|
|
22
|
+
});
|
|
23
|
+
if (observation.phase === "transaction") {
|
|
24
|
+
observability.incrementCounter({
|
|
25
|
+
name: "opengeni_workspace_delete_attempts_total",
|
|
26
|
+
help: "Workspace deletion attempts by terminal database outcome.",
|
|
27
|
+
labels: { outcome: observation.outcome },
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
for (const [kind, count] of Object.entries(observation.inventory ?? {})) {
|
|
31
|
+
observability.observeHistogram({
|
|
32
|
+
name: "opengeni_workspace_delete_inventory_rows",
|
|
33
|
+
help: "Rows or live owners observed by workspace deletion preflight inventory class.",
|
|
34
|
+
labels: { kind },
|
|
35
|
+
buckets: INVENTORY_BUCKETS,
|
|
36
|
+
value: Math.max(0, Math.floor(count ?? 0)),
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
observability.info("Workspace deletion phase", {
|
|
40
|
+
accountId: identity.accountId,
|
|
41
|
+
workspaceId: identity.workspaceId,
|
|
42
|
+
phase: observation.phase,
|
|
43
|
+
outcome: observation.outcome,
|
|
44
|
+
durationSeconds: observation.durationSeconds,
|
|
45
|
+
inventoryJson: JSON.stringify(observation.inventory ?? {}),
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Retain exact workspace deletion timing and inventory without putting tenant
|
|
51
|
+
* identifiers in metric labels. Observer failures never change deletion truth.
|
|
52
|
+
*/
|
|
53
|
+
export function workspaceDeleteObserver(
|
|
54
|
+
observability: Observability | null | undefined,
|
|
55
|
+
identity: { accountId: string; workspaceId: string },
|
|
56
|
+
): WorkspaceDeleteObserver | undefined {
|
|
57
|
+
if (!observability) return undefined;
|
|
58
|
+
return {
|
|
59
|
+
onPhase: (observation) => {
|
|
60
|
+
try {
|
|
61
|
+
recordWorkspaceDeleteObservation(observability, identity, observation);
|
|
62
|
+
} catch {
|
|
63
|
+
try {
|
|
64
|
+
observability.incrementCounter({
|
|
65
|
+
name: "opengeni_observability_observer_errors_total",
|
|
66
|
+
help: "Observability observer failures isolated from product execution.",
|
|
67
|
+
labels: { observer: "workspace_delete" },
|
|
68
|
+
});
|
|
69
|
+
} catch {
|
|
70
|
+
// The metrics registry itself is unhealthy. Deletion remains authoritative.
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
}
|