@opengeni/api-router 0.5.3 → 0.5.5
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.d.ts +9 -1
- package/dist/app.js +7 -1
- package/dist/{chunk-3HIA43CC.js → chunk-HBEJMWD3.js} +5470 -2223
- package/dist/chunk-HBEJMWD3.js.map +1 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +279 -55
- package/dist/index.js.map +1 -1
- package/package.json +20 -20
- package/src/app.ts +583 -166
- package/src/auth/managed-auth.ts +32 -16
- package/src/http/auth.ts +8 -1
- package/src/http/common.ts +6 -2
- package/src/http/sse.ts +84 -8
- package/src/index.ts +178 -75
- package/src/integrations/oauth-client.ts +403 -120
- package/src/integrations/provider-domain.ts +4 -1
- package/src/mcp/documents.ts +173 -94
- package/src/mcp/server.ts +1600 -693
- package/src/mcp/session-view.ts +8 -2
- package/src/mcp/toolspace.ts +175 -84
- package/src/observability.ts +7 -1
- package/src/routes/api-keys.ts +39 -23
- package/src/routes/billing.ts +180 -65
- package/src/routes/capabilities.ts +17 -8
- package/src/routes/catalog-assets.ts +5 -2
- package/src/routes/codex.ts +244 -63
- package/src/routes/connections.ts +71 -33
- package/src/routes/documents.ts +242 -92
- package/src/routes/enrollments.ts +100 -70
- package/src/routes/environments.ts +205 -136
- package/src/routes/files.ts +164 -39
- package/src/routes/github.ts +123 -50
- package/src/routes/install.ts +9 -2
- package/src/routes/machines.ts +9 -8
- package/src/routes/packs.ts +141 -89
- package/src/routes/rigs.ts +189 -0
- package/src/routes/scheduled-tasks.ts +51 -9
- package/src/routes/sessions.ts +870 -328
- package/src/routes/social.ts +50 -38
- package/src/routes/workspace-capture.ts +238 -0
- package/src/routes/workspaces.ts +146 -13
- package/src/sandbox/access.ts +11 -3
- package/src/sandbox/auth-callout.ts +5 -1
- package/src/sandbox/channel-a.ts +104 -27
- package/src/sandbox/enrollment.ts +13 -3
- package/src/sandbox/machines.ts +68 -59
- package/src/sandbox/metrics-ingestion.ts +238 -17
- package/src/sandbox/viewer.ts +172 -46
- package/dist/chunk-3HIA43CC.js.map +0 -1
package/src/auth/managed-auth.ts
CHANGED
|
@@ -27,12 +27,14 @@ export function createManagedAuth(settings: Settings, db: Database): ManagedAuth
|
|
|
27
27
|
trustedOrigins: betterAuthTrustedOrigins(settings),
|
|
28
28
|
advanced: {
|
|
29
29
|
useSecureCookies: settings.publicBaseUrl?.startsWith("https://") ?? false,
|
|
30
|
-
...(settings.betterAuthCookieDomain
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
30
|
+
...(settings.betterAuthCookieDomain
|
|
31
|
+
? {
|
|
32
|
+
crossSubDomainCookies: {
|
|
33
|
+
enabled: true,
|
|
34
|
+
domain: settings.betterAuthCookieDomain,
|
|
35
|
+
},
|
|
36
|
+
}
|
|
37
|
+
: {}),
|
|
36
38
|
database: {
|
|
37
39
|
generateId: () => crypto.randomUUID(),
|
|
38
40
|
},
|
|
@@ -149,7 +151,11 @@ export function createManagedAuth(settings: Settings, db: Database): ManagedAuth
|
|
|
149
151
|
}) as ManagedAuth;
|
|
150
152
|
}
|
|
151
153
|
|
|
152
|
-
export async function managedSessionAccessContext(
|
|
154
|
+
export async function managedSessionAccessContext(
|
|
155
|
+
auth: ManagedAuth,
|
|
156
|
+
db: Database,
|
|
157
|
+
headers: Headers,
|
|
158
|
+
) {
|
|
153
159
|
const session = await auth.api.getSession({ headers });
|
|
154
160
|
if (!session?.user) {
|
|
155
161
|
return null;
|
|
@@ -184,15 +190,20 @@ function betterAuthTrustedOrigins(settings: Settings): string[] {
|
|
|
184
190
|
return [...origins];
|
|
185
191
|
}
|
|
186
192
|
|
|
187
|
-
async function sendEmail(
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
+
async function sendEmail(
|
|
194
|
+
settings: Settings,
|
|
195
|
+
input: {
|
|
196
|
+
to: string;
|
|
197
|
+
subject: string;
|
|
198
|
+
text: string;
|
|
199
|
+
html: string;
|
|
200
|
+
},
|
|
201
|
+
): Promise<void> {
|
|
193
202
|
if (!settings.resendApiKey) {
|
|
194
203
|
if (settings.environment === "local" || settings.environment === "test") {
|
|
195
|
-
console.warn(
|
|
204
|
+
console.warn(
|
|
205
|
+
`[opengeni] Skipping email to ${input.to}: OPENGENI_RESEND_API_KEY is not configured`,
|
|
206
|
+
);
|
|
196
207
|
return;
|
|
197
208
|
}
|
|
198
209
|
throw new Error("OPENGENI_RESEND_API_KEY is required to send managed auth email");
|
|
@@ -212,7 +223,9 @@ async function sendEmail(settings: Settings, input: {
|
|
|
212
223
|
|
|
213
224
|
async function verificationUrl(settings: Settings, email: string): Promise<string> {
|
|
214
225
|
if (!settings.betterAuthSecret) {
|
|
215
|
-
throw new Error(
|
|
226
|
+
throw new Error(
|
|
227
|
+
"OPENGENI_BETTER_AUTH_SECRET is required to send managed auth verification email",
|
|
228
|
+
);
|
|
216
229
|
}
|
|
217
230
|
if (!settings.publicBaseUrl) {
|
|
218
231
|
throw new Error("OPENGENI_PUBLIC_BASE_URL is required to send managed auth verification email");
|
|
@@ -225,7 +238,10 @@ async function verificationUrl(settings: Settings, email: string): Promise<strin
|
|
|
225
238
|
}
|
|
226
239
|
|
|
227
240
|
function splitCsv(raw: string): string[] {
|
|
228
|
-
return raw
|
|
241
|
+
return raw
|
|
242
|
+
.split(",")
|
|
243
|
+
.map((value) => value.trim())
|
|
244
|
+
.filter(Boolean);
|
|
229
245
|
}
|
|
230
246
|
|
|
231
247
|
function escapeHtml(value: string): string {
|
package/src/http/auth.ts
CHANGED
|
@@ -84,7 +84,14 @@ function isAuthorized(c: Context, expected: string | undefined): boolean {
|
|
|
84
84
|
return false;
|
|
85
85
|
}
|
|
86
86
|
const explicit = c.req.header("x-opengeni-access-key");
|
|
87
|
-
|
|
87
|
+
if (constantTimeEqual(explicit, expected)) {
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
const authorization = c.req.header("authorization");
|
|
91
|
+
const bearer = authorization?.startsWith("Bearer ")
|
|
92
|
+
? authorization.slice("Bearer ".length)
|
|
93
|
+
: undefined;
|
|
94
|
+
return constantTimeEqual(bearer, expected);
|
|
88
95
|
}
|
|
89
96
|
|
|
90
97
|
function constantTimeEqual(actual: string | undefined, expected: string): boolean {
|
package/src/http/common.ts
CHANGED
|
@@ -9,8 +9,12 @@ export function boundedLimit(raw: string | undefined): number {
|
|
|
9
9
|
return Math.min(500, Math.max(1, Math.floor(limit)));
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
export async function assertSessionExists(
|
|
13
|
-
|
|
12
|
+
export async function assertSessionExists(
|
|
13
|
+
db: Database,
|
|
14
|
+
workspaceId: string,
|
|
15
|
+
sessionId: string,
|
|
16
|
+
): Promise<void> {
|
|
17
|
+
if (!(await getSession(db, workspaceId, sessionId))) {
|
|
14
18
|
throw new HTTPException(404, { message: "session not found" });
|
|
15
19
|
}
|
|
16
20
|
}
|
package/src/http/sse.ts
CHANGED
|
@@ -1,8 +1,15 @@
|
|
|
1
|
-
import type { SessionEvent } from "@opengeni/contracts";
|
|
2
|
-
import { listSessionEvents, type Database } from "@opengeni/db";
|
|
1
|
+
import type { SessionEvent, WorkspaceControlEvent } from "@opengeni/contracts";
|
|
2
|
+
import { listSessionEvents, listWorkspaceControlEvents, type Database } from "@opengeni/db";
|
|
3
3
|
import { formatSse, type EventBus } from "@opengeni/events";
|
|
4
4
|
|
|
5
|
-
export async function sseSessionStream(
|
|
5
|
+
export async function sseSessionStream(
|
|
6
|
+
db: Database,
|
|
7
|
+
bus: EventBus,
|
|
8
|
+
workspaceId: string,
|
|
9
|
+
sessionId: string,
|
|
10
|
+
after: number,
|
|
11
|
+
signal: AbortSignal,
|
|
12
|
+
): Promise<Response> {
|
|
6
13
|
const encoder = new TextEncoder();
|
|
7
14
|
let controller: ReadableStreamDefaultController<Uint8Array>;
|
|
8
15
|
let lastSent = after;
|
|
@@ -18,7 +25,13 @@ export async function sseSessionStream(db: Database, bus: EventBus, workspaceId:
|
|
|
18
25
|
return;
|
|
19
26
|
}
|
|
20
27
|
if (event.sequence > lastSent + 1) {
|
|
21
|
-
const missing = await listSessionEvents(
|
|
28
|
+
const missing = await listSessionEvents(
|
|
29
|
+
db,
|
|
30
|
+
workspaceId,
|
|
31
|
+
sessionId,
|
|
32
|
+
lastSent,
|
|
33
|
+
event.sequence - lastSent - 1,
|
|
34
|
+
);
|
|
22
35
|
for (const missed of missing) {
|
|
23
36
|
if (missed.sequence > lastSent) {
|
|
24
37
|
controller.enqueue(encoder.encode(formatSse(missed)));
|
|
@@ -40,7 +53,11 @@ export async function sseSessionStream(db: Database, bus: EventBus, workspaceId:
|
|
|
40
53
|
}
|
|
41
54
|
});
|
|
42
55
|
|
|
43
|
-
await replaySessionEvents(
|
|
56
|
+
await replaySessionEvents(
|
|
57
|
+
(cursor, limit) => listSessionEvents(db, workspaceId, sessionId, cursor, limit),
|
|
58
|
+
send,
|
|
59
|
+
after,
|
|
60
|
+
);
|
|
44
61
|
replaying = false;
|
|
45
62
|
for (const event of buffered.sort((a, b) => a.sequence - b.sequence)) {
|
|
46
63
|
await send(event);
|
|
@@ -53,9 +70,13 @@ export async function sseSessionStream(db: Database, bus: EventBus, workspaceId:
|
|
|
53
70
|
},
|
|
54
71
|
});
|
|
55
72
|
|
|
56
|
-
signal.addEventListener(
|
|
57
|
-
|
|
58
|
-
|
|
73
|
+
signal.addEventListener(
|
|
74
|
+
"abort",
|
|
75
|
+
() => {
|
|
76
|
+
unsubscribe?.();
|
|
77
|
+
},
|
|
78
|
+
{ once: true },
|
|
79
|
+
);
|
|
59
80
|
|
|
60
81
|
return new Response(stream, {
|
|
61
82
|
headers: {
|
|
@@ -87,3 +108,58 @@ export async function replaySessionEvents(
|
|
|
87
108
|
}
|
|
88
109
|
}
|
|
89
110
|
}
|
|
111
|
+
|
|
112
|
+
export async function sseWorkspaceControlStream(
|
|
113
|
+
db: Database,
|
|
114
|
+
bus: EventBus,
|
|
115
|
+
workspaceId: string,
|
|
116
|
+
after: number,
|
|
117
|
+
signal: AbortSignal,
|
|
118
|
+
): Promise<Response> {
|
|
119
|
+
const encoder = new TextEncoder();
|
|
120
|
+
let lastSent = after;
|
|
121
|
+
let replaying = true;
|
|
122
|
+
const buffered: WorkspaceControlEvent[] = [];
|
|
123
|
+
let unsubscribe: (() => void) | null = null;
|
|
124
|
+
|
|
125
|
+
const stream = new ReadableStream<Uint8Array>({
|
|
126
|
+
start: async (controller) => {
|
|
127
|
+
const send = (event: WorkspaceControlEvent) => {
|
|
128
|
+
if (event.sequence <= lastSent) return;
|
|
129
|
+
controller.enqueue(encoder.encode(formatSse(event)));
|
|
130
|
+
lastSent = event.sequence;
|
|
131
|
+
};
|
|
132
|
+
unsubscribe = await bus.subscribeWorkspaceControl(workspaceId, async (event) => {
|
|
133
|
+
if (replaying) {
|
|
134
|
+
buffered.push(event);
|
|
135
|
+
} else {
|
|
136
|
+
send(event);
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
let cursor = after;
|
|
140
|
+
while (true) {
|
|
141
|
+
const page = await listWorkspaceControlEvents(db, workspaceId, cursor, 1000);
|
|
142
|
+
for (const event of page) {
|
|
143
|
+
send(event);
|
|
144
|
+
cursor = Math.max(cursor, event.sequence);
|
|
145
|
+
}
|
|
146
|
+
if (page.length < 1000) break;
|
|
147
|
+
}
|
|
148
|
+
replaying = false;
|
|
149
|
+
for (const event of buffered.sort((left, right) => left.sequence - right.sequence)) {
|
|
150
|
+
send(event);
|
|
151
|
+
}
|
|
152
|
+
buffered.length = 0;
|
|
153
|
+
controller.enqueue(encoder.encode(": connected\n\n"));
|
|
154
|
+
},
|
|
155
|
+
cancel: () => unsubscribe?.(),
|
|
156
|
+
});
|
|
157
|
+
signal.addEventListener("abort", () => unsubscribe?.(), { once: true });
|
|
158
|
+
return new Response(stream, {
|
|
159
|
+
headers: {
|
|
160
|
+
"Content-Type": "text/event-stream; charset=utf-8",
|
|
161
|
+
"Cache-Control": "no-cache, no-transform",
|
|
162
|
+
Connection: "keep-alive",
|
|
163
|
+
},
|
|
164
|
+
});
|
|
165
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,9 +1,27 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import {
|
|
2
|
+
dbSearchPath,
|
|
3
|
+
getSettings,
|
|
4
|
+
resolveNatsCalloutConfig,
|
|
5
|
+
resolveNatsControlPlaneAuth,
|
|
6
|
+
retryStartupDependency,
|
|
7
|
+
startupRetryOptions,
|
|
8
|
+
} from "@opengeni/config";
|
|
9
|
+
import type {
|
|
10
|
+
ScheduledTask,
|
|
11
|
+
ScheduledTaskOverlapPolicy,
|
|
12
|
+
ScheduledTaskScheduleSpec,
|
|
13
|
+
} from "@opengeni/contracts";
|
|
14
|
+
import { createDb, markSessionWorkflowWakeDelivered, type Database } from "@opengeni/db";
|
|
4
15
|
import { createNatsEventBus, type ResponderConnection } from "@opengeni/events";
|
|
5
16
|
import { createObservability, logStartupDependencyRetry } from "@opengeni/observability";
|
|
6
|
-
import {
|
|
17
|
+
import { SESSION_WORKFLOW_WAKE_DISPATCHER_SCHEDULE_ID } from "@opengeni/core";
|
|
18
|
+
import {
|
|
19
|
+
Connection,
|
|
20
|
+
Client as TemporalClient,
|
|
21
|
+
ScheduleNotFoundError,
|
|
22
|
+
ScheduleOverlapPolicy,
|
|
23
|
+
WorkflowExecutionAlreadyStartedError,
|
|
24
|
+
} from "@temporalio/client";
|
|
7
25
|
import type { ScheduleOptions, ScheduleSpec, ScheduleUpdateOptions } from "@temporalio/client";
|
|
8
26
|
import { createApp, type DocumentIndexClient, type SessionWorkflowClient } from "./app";
|
|
9
27
|
import { observabilityEventLogger } from "./observability";
|
|
@@ -35,7 +53,10 @@ const TEMPORAL_MONTHS = [
|
|
|
35
53
|
"DECEMBER",
|
|
36
54
|
] as const;
|
|
37
55
|
|
|
38
|
-
export async function createTemporalWorkflowClient(
|
|
56
|
+
export async function createTemporalWorkflowClient(
|
|
57
|
+
settings: ReturnType<typeof getSettings>,
|
|
58
|
+
db: Database,
|
|
59
|
+
): Promise<{
|
|
39
60
|
client: SessionWorkflowClient;
|
|
40
61
|
documentIndexer: DocumentIndexClient;
|
|
41
62
|
close: () => Promise<void>;
|
|
@@ -49,34 +70,81 @@ export async function createTemporalWorkflowClient(settings: ReturnType<typeof g
|
|
|
49
70
|
signalUserMessage: async ({ eventId, workflowId }) => {
|
|
50
71
|
await temporal.workflow.getHandle(workflowId).signal("userMessage", eventId);
|
|
51
72
|
},
|
|
52
|
-
wakeSessionWorkflow: async ({
|
|
73
|
+
wakeSessionWorkflow: async ({
|
|
74
|
+
accountId,
|
|
75
|
+
workspaceId,
|
|
76
|
+
sessionId,
|
|
77
|
+
workflowId,
|
|
78
|
+
wakeRevision,
|
|
79
|
+
interruptionRequested,
|
|
80
|
+
}) => {
|
|
53
81
|
await temporal.workflow.signalWithStart("sessionWorkflow", {
|
|
54
82
|
taskQueue: settings.temporalTaskQueue,
|
|
55
83
|
workflowId,
|
|
56
84
|
workflowIdReusePolicy: "ALLOW_DUPLICATE",
|
|
57
85
|
args: [{ accountId, workspaceId, sessionId }],
|
|
58
|
-
signal: "queueChanged",
|
|
86
|
+
signal: interruptionRequested ? "sessionControl" : "queueChanged",
|
|
87
|
+
});
|
|
88
|
+
await markSessionWorkflowWakeDelivered(db, {
|
|
89
|
+
accountId,
|
|
90
|
+
workspaceId,
|
|
91
|
+
sessionId,
|
|
92
|
+
temporalWorkflowId: workflowId,
|
|
93
|
+
wakeRevision,
|
|
59
94
|
});
|
|
60
95
|
},
|
|
61
|
-
|
|
62
|
-
await temporal.
|
|
96
|
+
requestSessionWorkflowWakeDispatch: async () => {
|
|
97
|
+
await temporal.schedule
|
|
98
|
+
.getHandle(SESSION_WORKFLOW_WAKE_DISPATCHER_SCHEDULE_ID)
|
|
99
|
+
.trigger(ScheduleOverlapPolicy.BUFFER_ONE);
|
|
63
100
|
},
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
101
|
+
signalCodexCapacity: async ({
|
|
102
|
+
accountId,
|
|
103
|
+
workspaceId,
|
|
104
|
+
sessionId,
|
|
105
|
+
workflowId,
|
|
106
|
+
wakeRevision,
|
|
107
|
+
workflowWakeRevision,
|
|
108
|
+
}) => {
|
|
72
109
|
await temporal.workflow.signalWithStart("sessionWorkflow", {
|
|
73
110
|
taskQueue: settings.temporalTaskQueue,
|
|
74
111
|
workflowId,
|
|
75
112
|
workflowIdReusePolicy: "ALLOW_DUPLICATE",
|
|
76
113
|
args: [{ accountId, workspaceId, sessionId }],
|
|
77
|
-
signal: "
|
|
114
|
+
signal: "codexCapacityChanged",
|
|
115
|
+
signalArgs: [wakeRevision],
|
|
116
|
+
});
|
|
117
|
+
await markSessionWorkflowWakeDelivered(db, {
|
|
118
|
+
accountId,
|
|
119
|
+
workspaceId,
|
|
120
|
+
sessionId,
|
|
121
|
+
temporalWorkflowId: workflowId,
|
|
122
|
+
wakeRevision: workflowWakeRevision,
|
|
123
|
+
});
|
|
124
|
+
},
|
|
125
|
+
signalApprovalDecision: async ({
|
|
126
|
+
accountId,
|
|
127
|
+
workspaceId,
|
|
128
|
+
sessionId,
|
|
129
|
+
eventId,
|
|
130
|
+
workflowId,
|
|
131
|
+
workflowWakeRevision,
|
|
132
|
+
}) => {
|
|
133
|
+
await temporal.workflow.signalWithStart("sessionWorkflow", {
|
|
134
|
+
taskQueue: settings.temporalTaskQueue,
|
|
135
|
+
workflowId,
|
|
136
|
+
workflowIdReusePolicy: "ALLOW_DUPLICATE",
|
|
137
|
+
args: [{ accountId, workspaceId, sessionId }],
|
|
138
|
+
signal: "approvalDecision",
|
|
78
139
|
signalArgs: [eventId],
|
|
79
140
|
});
|
|
141
|
+
await markSessionWorkflowWakeDelivered(db, {
|
|
142
|
+
accountId,
|
|
143
|
+
workspaceId,
|
|
144
|
+
sessionId,
|
|
145
|
+
temporalWorkflowId: workflowId,
|
|
146
|
+
wakeRevision: workflowWakeRevision,
|
|
147
|
+
});
|
|
80
148
|
},
|
|
81
149
|
syncScheduledTask: async ({ task }) => {
|
|
82
150
|
const schedule = temporal.schedule.getHandle(task.temporalScheduleId);
|
|
@@ -91,36 +159,60 @@ export async function createTemporalWorkflowClient(settings: ReturnType<typeof g
|
|
|
91
159
|
}
|
|
92
160
|
},
|
|
93
161
|
deleteScheduledTaskSchedule: async ({ temporalScheduleId }) => {
|
|
94
|
-
await temporal.schedule
|
|
162
|
+
await temporal.schedule
|
|
163
|
+
.getHandle(temporalScheduleId)
|
|
164
|
+
.delete()
|
|
165
|
+
.catch(() => undefined);
|
|
95
166
|
},
|
|
96
167
|
triggerScheduledTask: async ({ task, agentRunUsageIdempotencyKey, triggerWorkflowId }) => {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
168
|
+
// Deterministic workflowId (derived from the trigger token by the
|
|
169
|
+
// caller) + REJECT_DUPLICATE makes a retried manual trigger idempotent:
|
|
170
|
+
// the second start collides on the id and is rejected instead of
|
|
171
|
+
// spawning a second run. The shared idempotency key dedupes the charge.
|
|
172
|
+
const workflowId =
|
|
173
|
+
triggerWorkflowId ?? `scheduled-task-${task.id}-manual-${crypto.randomUUID()}`;
|
|
174
|
+
try {
|
|
175
|
+
await temporal.workflow.start("scheduledTaskFireWorkflow", {
|
|
176
|
+
taskQueue: settings.temporalTaskQueue,
|
|
177
|
+
workflowId,
|
|
178
|
+
workflowIdReusePolicy: "REJECT_DUPLICATE",
|
|
179
|
+
args: [
|
|
180
|
+
{
|
|
181
|
+
accountId: task.accountId,
|
|
182
|
+
workspaceId: task.workspaceId,
|
|
183
|
+
taskId: task.id,
|
|
184
|
+
triggerType: "manual",
|
|
185
|
+
agentRunUsageIdempotencyKey,
|
|
186
|
+
},
|
|
187
|
+
],
|
|
188
|
+
});
|
|
189
|
+
} catch (error) {
|
|
190
|
+
// A duplicate trigger token started this run already; treat the retry
|
|
191
|
+
// as a no-op so the (idempotent) usage charge stays the only effect.
|
|
192
|
+
if (isWorkflowAlreadyStarted(error)) {
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
121
195
|
throw error;
|
|
122
196
|
}
|
|
123
197
|
},
|
|
198
|
+
startRigVerification: async ({ workspaceId, changeId, versionId, workflowId }) => {
|
|
199
|
+
const targetId = changeId ?? versionId;
|
|
200
|
+
if (!targetId) {
|
|
201
|
+
throw new Error("rig verification requires changeId or versionId");
|
|
202
|
+
}
|
|
203
|
+
await temporal.workflow.start("rigVerificationWorkflow", {
|
|
204
|
+
taskQueue: settings.temporalTaskQueue,
|
|
205
|
+
workflowId: workflowId ?? `rig-verification-${targetId}-${crypto.randomUUID()}`,
|
|
206
|
+
workflowIdReusePolicy: "ALLOW_DUPLICATE",
|
|
207
|
+
args: [
|
|
208
|
+
{
|
|
209
|
+
workspaceId,
|
|
210
|
+
...(changeId ? { changeId } : {}),
|
|
211
|
+
...(versionId ? { versionId } : {}),
|
|
212
|
+
},
|
|
213
|
+
],
|
|
214
|
+
});
|
|
215
|
+
},
|
|
124
216
|
check: async () => {
|
|
125
217
|
await connection.workflowService.getSystemInfo({});
|
|
126
218
|
},
|
|
@@ -158,7 +250,8 @@ export async function startApi() {
|
|
|
158
250
|
let bus: Awaited<ReturnType<typeof createNatsEventBus>> | undefined;
|
|
159
251
|
let workflowClient: Awaited<ReturnType<typeof createTemporalWorkflowClient>> | undefined;
|
|
160
252
|
const retryOptions = startupRetryOptions(settings);
|
|
161
|
-
const onRetry = (event: Parameters<typeof logStartupDependencyRetry>[1]) =>
|
|
253
|
+
const onRetry = (event: Parameters<typeof logStartupDependencyRetry>[1]) =>
|
|
254
|
+
logStartupDependencyRetry(observability, event);
|
|
162
255
|
// The PRIVILEGED control-plane NATS login (M-AUTH): when the server runs with
|
|
163
256
|
// auth_callout, api/worker authenticate as a static account user permitted to
|
|
164
257
|
// request `agent.*.rpc`. Null in local dev (anonymous connect — the bus default).
|
|
@@ -169,7 +262,9 @@ export async function startApi() {
|
|
|
169
262
|
() =>
|
|
170
263
|
createNatsEventBus(
|
|
171
264
|
settings.natsUrl,
|
|
172
|
-
controlPlaneAuth
|
|
265
|
+
controlPlaneAuth
|
|
266
|
+
? { user: controlPlaneAuth.user, pass: controlPlaneAuth.password }
|
|
267
|
+
: undefined,
|
|
173
268
|
{ logger: observabilityEventLogger(observability) },
|
|
174
269
|
),
|
|
175
270
|
{
|
|
@@ -177,16 +272,16 @@ export async function startApi() {
|
|
|
177
272
|
onRetry,
|
|
178
273
|
},
|
|
179
274
|
);
|
|
180
|
-
workflowClient = await retryStartupDependency(
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
275
|
+
workflowClient = await retryStartupDependency(
|
|
276
|
+
"Temporal",
|
|
277
|
+
() => createTemporalWorkflowClient(settings, dbClient.db),
|
|
278
|
+
{
|
|
279
|
+
...retryOptions,
|
|
280
|
+
onRetry,
|
|
281
|
+
},
|
|
282
|
+
);
|
|
184
283
|
} catch (error) {
|
|
185
|
-
await Promise.allSettled([
|
|
186
|
-
bus?.close(),
|
|
187
|
-
workflowClient?.close(),
|
|
188
|
-
dbClient.close(),
|
|
189
|
-
]);
|
|
284
|
+
await Promise.allSettled([bus?.close(), workflowClient?.close(), dbClient.close()]);
|
|
190
285
|
throw error;
|
|
191
286
|
}
|
|
192
287
|
if (!bus || !workflowClient) {
|
|
@@ -295,25 +390,29 @@ export function temporalScheduleSpec(schedule: ScheduledTaskScheduleSpec): Sched
|
|
|
295
390
|
}
|
|
296
391
|
if (schedule.type === "calendar") {
|
|
297
392
|
return {
|
|
298
|
-
calendars: [
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
393
|
+
calendars: [
|
|
394
|
+
{
|
|
395
|
+
hour: schedule.hour,
|
|
396
|
+
minute: schedule.minute,
|
|
397
|
+
second: 0,
|
|
398
|
+
...(schedule.daysOfWeek ? { dayOfWeek: schedule.daysOfWeek } : {}),
|
|
399
|
+
},
|
|
400
|
+
],
|
|
304
401
|
timezone: schedule.timeZone,
|
|
305
402
|
};
|
|
306
403
|
}
|
|
307
404
|
const runAt = new Date(schedule.runAt);
|
|
308
405
|
return {
|
|
309
|
-
calendars: [
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
406
|
+
calendars: [
|
|
407
|
+
{
|
|
408
|
+
year: runAt.getUTCFullYear(),
|
|
409
|
+
month: temporalMonth(runAt.getUTCMonth()),
|
|
410
|
+
dayOfMonth: runAt.getUTCDate(),
|
|
411
|
+
hour: runAt.getUTCHours(),
|
|
412
|
+
minute: runAt.getUTCMinutes(),
|
|
413
|
+
second: runAt.getUTCSeconds(),
|
|
414
|
+
},
|
|
415
|
+
],
|
|
317
416
|
timezone: "UTC",
|
|
318
417
|
};
|
|
319
418
|
}
|
|
@@ -330,12 +429,14 @@ function temporalScheduleOptions(task: ScheduledTask, taskQueue: string): Schedu
|
|
|
330
429
|
type: "startWorkflow",
|
|
331
430
|
workflowType: "scheduledTaskFireWorkflow",
|
|
332
431
|
taskQueue,
|
|
333
|
-
args: [
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
432
|
+
args: [
|
|
433
|
+
{
|
|
434
|
+
accountId: task.accountId,
|
|
435
|
+
workspaceId: task.workspaceId,
|
|
436
|
+
taskId: task.id,
|
|
437
|
+
triggerType: "scheduled",
|
|
438
|
+
},
|
|
439
|
+
],
|
|
339
440
|
},
|
|
340
441
|
policies: {
|
|
341
442
|
overlap: temporalOverlapPolicy(task.overlapPolicy),
|
|
@@ -362,6 +463,8 @@ function temporalScheduleUpdateOptions(options: ScheduleOptions): ScheduleUpdate
|
|
|
362
463
|
...(options.policies ? { policies: options.policies } : {}),
|
|
363
464
|
state: options.state ?? {},
|
|
364
465
|
...(options.searchAttributes ? { searchAttributes: options.searchAttributes } : {}),
|
|
365
|
-
...(options.typedSearchAttributes
|
|
466
|
+
...(options.typedSearchAttributes
|
|
467
|
+
? { typedSearchAttributes: options.typedSearchAttributes }
|
|
468
|
+
: {}),
|
|
366
469
|
};
|
|
367
470
|
}
|