@opengeni/api-router 0.11.8 → 0.12.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/app.js +1 -1
- package/dist/{chunk-DQWFAIPE.js → chunk-S2N4252E.js} +1526 -453
- package/dist/chunk-S2N4252E.js.map +1 -0
- package/dist/index.js +1 -1
- package/package.json +10 -10
- package/src/app.ts +34 -24
- package/src/http/auth.ts +4 -1
- package/src/integrations/slack-bot.ts +694 -0
- package/src/mcp/server.ts +120 -0
- package/src/routes/connections.ts +155 -1
- package/src/routes/sessions.ts +10 -0
- package/src/routes/workspace-instruction-policies.ts +243 -0
- package/src/sandbox/channel-a.ts +131 -70
- package/dist/chunk-DQWFAIPE.js.map +0 -1
package/src/sandbox/channel-a.ts
CHANGED
|
@@ -3,16 +3,16 @@
|
|
|
3
3
|
// The structured services (FileSystem / Git / Terminal) are SYNCHRONOUS point
|
|
4
4
|
// queries served client -> API -> box IN-PROCESS. Each call:
|
|
5
5
|
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
6
|
+
// For a provider-backed home it acquires an exact direct-request lease holder,
|
|
7
|
+
// resumes the box by id, and releases the holder after the operation. For a
|
|
8
|
+
// Connected Machine home it follows the durable active pointer directly and
|
|
9
|
+
// uses its NATS request/reply control channel; it creates no phantom cloud lease.
|
|
10
|
+
// Both paths build one SandboxChannelAService, run the operation, and return the
|
|
11
|
+
// result inline.
|
|
12
12
|
//
|
|
13
|
-
// NO Temporal
|
|
14
|
-
//
|
|
15
|
-
// (fs.changed/git.changed/terminal.pty.*) ride A1
|
|
13
|
+
// NO Temporal or worker RPC sits in this path. Provider-backed reads remain
|
|
14
|
+
// process-local; Connected Machine operations necessarily ride NATS. Side-effect
|
|
15
|
+
// notifications (fs.changed/git.changed/terminal.pty.*) ride A1.
|
|
16
16
|
//
|
|
17
17
|
// IMPORT DISCIPLINE: sandbox symbols come ONLY from @opengeni/runtime/sandbox
|
|
18
18
|
// (the agent-loop-free leaf) — enforced by sandbox-access-import-guard.test.ts.
|
|
@@ -29,8 +29,10 @@ import type { Session } from "@opengeni/contracts";
|
|
|
29
29
|
import {
|
|
30
30
|
acquireLease,
|
|
31
31
|
getSandboxSessionEnvelope,
|
|
32
|
+
getSandbox,
|
|
32
33
|
loadWorkspaceEnvironmentForRun,
|
|
33
34
|
markWarmLeaseInstanceLost,
|
|
35
|
+
readActiveSandbox,
|
|
34
36
|
readLease,
|
|
35
37
|
releaseLeaseHolder,
|
|
36
38
|
type Database,
|
|
@@ -40,9 +42,11 @@ import { appendAndPublishEvents, type EventBus } from "@opengeni/events";
|
|
|
40
42
|
import { HTTPException } from "hono/http-exception";
|
|
41
43
|
|
|
42
44
|
import {
|
|
45
|
+
buildSelfhostedBackendSession,
|
|
43
46
|
establishSandboxSessionFromEnvelope,
|
|
44
47
|
isProviderSandboxNotFoundError,
|
|
45
48
|
SandboxChannelAService,
|
|
49
|
+
NatsControlRpc,
|
|
46
50
|
ChannelAConflictError,
|
|
47
51
|
ChannelANotFoundError,
|
|
48
52
|
ChannelAUnsupportedError,
|
|
@@ -55,7 +59,7 @@ import {
|
|
|
55
59
|
type EstablishedSandboxSession,
|
|
56
60
|
type RoutingSandboxSession,
|
|
57
61
|
} from "@opengeni/runtime/sandbox";
|
|
58
|
-
import { wrapChannelABoxWithRouting } from "@opengeni/core";
|
|
62
|
+
import { relayConfigFromSettings, wrapChannelABoxWithRouting } from "@opengeni/core";
|
|
59
63
|
import { establishApiSandboxSpawner } from "./rematerialize";
|
|
60
64
|
|
|
61
65
|
export type ChannelAServices = {
|
|
@@ -76,7 +80,9 @@ export type ChannelAContext = {
|
|
|
76
80
|
// (for the pty exec-session epoch fence + revision seeding).
|
|
77
81
|
export type ChannelAHandle = {
|
|
78
82
|
service: SandboxChannelAService;
|
|
79
|
-
lease
|
|
83
|
+
/** Connected Machine homes deliberately have no cloud lease. Durable PTYs
|
|
84
|
+
* require a real home-provider lease and reject this null case. */
|
|
85
|
+
lease: LeaseSnapshot | null;
|
|
80
86
|
routingSession: RoutingSandboxSession;
|
|
81
87
|
requestId: string;
|
|
82
88
|
};
|
|
@@ -107,6 +113,117 @@ export async function withChannelA<T>(
|
|
|
107
113
|
const holderId = `direct:${requestId}`;
|
|
108
114
|
const leaseTtlMs = settings.sandboxLeaseTtlMs;
|
|
109
115
|
|
|
116
|
+
// The STABLE run-environment used by both a cloud home and a machine home.
|
|
117
|
+
// It also carries the per-session Toolspace pointer selected below.
|
|
118
|
+
const workspaceEnvironment = await loadWorkspaceEnvironmentForRun(
|
|
119
|
+
db,
|
|
120
|
+
settings,
|
|
121
|
+
workspaceId,
|
|
122
|
+
session.environmentId,
|
|
123
|
+
);
|
|
124
|
+
const settingsForSession =
|
|
125
|
+
session.sandboxBackend !== settings.sandboxBackend
|
|
126
|
+
? { ...settings, sandboxBackend: session.sandboxBackend }
|
|
127
|
+
: settings;
|
|
128
|
+
const environment = stableSandboxEnvironmentForRun(
|
|
129
|
+
settingsForSession,
|
|
130
|
+
workspaceEnvironment?.values ?? {},
|
|
131
|
+
{ workspaceId },
|
|
132
|
+
);
|
|
133
|
+
if (hasGitCredentialRepositorySelection(session.resources)) {
|
|
134
|
+
applyGitAuthPointerEnvironment(
|
|
135
|
+
environment,
|
|
136
|
+
hasGitHubRepositorySelection(session.resources) ? githubAppBotIdentity(settings) : null,
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const runEstablished = async (
|
|
141
|
+
routed: EstablishedSandboxSession,
|
|
142
|
+
lease: LeaseSnapshot | null,
|
|
143
|
+
): Promise<T> => {
|
|
144
|
+
const emit = async (events: { type: string; payload: unknown }[]): Promise<void> => {
|
|
145
|
+
await appendAndPublishEvents(
|
|
146
|
+
db,
|
|
147
|
+
bus,
|
|
148
|
+
workspaceId,
|
|
149
|
+
session.id,
|
|
150
|
+
events.map((e) => ({ type: e.type as never, payload: e.payload })),
|
|
151
|
+
);
|
|
152
|
+
};
|
|
153
|
+
const routingSession = routed.session as RoutingSandboxSession;
|
|
154
|
+
const credentialSession = withRunCredentialsSession(routingSession as object, session.id);
|
|
155
|
+
const scopedSession = environment.OPENGENI_TOOLSPACE_TOKEN_FILE
|
|
156
|
+
? withToolspaceTokenSession(
|
|
157
|
+
credentialSession,
|
|
158
|
+
toolspaceTokenFileFromEnvironment(environment, session.id),
|
|
159
|
+
)
|
|
160
|
+
: credentialSession;
|
|
161
|
+
const service = new SandboxChannelAService({
|
|
162
|
+
session: scopedSession as ChannelASession,
|
|
163
|
+
leaseEpoch: lease?.leaseEpoch ?? session.activeEpoch,
|
|
164
|
+
emit,
|
|
165
|
+
});
|
|
166
|
+
return await fn({ service, lease, routingSession, requestId });
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
// A machine-targeted top-level session has an honest selfhosted HOME label.
|
|
170
|
+
// It has no cloud provider box and therefore must not acquire or establish a
|
|
171
|
+
// phantom home lease before following its active machine pointer.
|
|
172
|
+
if (session.sandboxBackend === "selfhosted") {
|
|
173
|
+
let established: EstablishedSandboxSession | undefined;
|
|
174
|
+
try {
|
|
175
|
+
const pointer = await readActiveSandbox(db, workspaceId, session.id);
|
|
176
|
+
if (!pointer?.activeSandboxId) {
|
|
177
|
+
throw new HTTPException(409, {
|
|
178
|
+
message: "machine-home session has no active Connected Machine",
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
const sandbox = await getSandbox(db, workspaceId, pointer.activeSandboxId);
|
|
182
|
+
if (sandbox?.kind !== "selfhosted" || !sandbox.enrollmentId) {
|
|
183
|
+
throw new HTTPException(409, {
|
|
184
|
+
message: "machine-home session points to an unavailable Connected Machine",
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
const built = await buildSelfhostedBackendSession({
|
|
188
|
+
workspaceId,
|
|
189
|
+
agentId: sandbox.enrollmentId,
|
|
190
|
+
relay: relayConfigFromSettings(settings),
|
|
191
|
+
controlRpcFactory: () => new NatsControlRpc(async () => bus.getRequestConnection()),
|
|
192
|
+
epoch: pointer.activeEpoch,
|
|
193
|
+
environment,
|
|
194
|
+
workingDir: pointer.workingDir,
|
|
195
|
+
timeoutMs: settings.sandboxSelfhostedControlTimeoutMs,
|
|
196
|
+
execTimeoutMs: settings.sandboxSelfhostedExecTimeoutMs,
|
|
197
|
+
});
|
|
198
|
+
established = {
|
|
199
|
+
client: built.client,
|
|
200
|
+
session: built.session,
|
|
201
|
+
sessionState: { agentId: sandbox.enrollmentId },
|
|
202
|
+
instanceId: sandbox.enrollmentId,
|
|
203
|
+
backendId: "selfhosted",
|
|
204
|
+
};
|
|
205
|
+
const routed = wrapChannelABoxWithRouting(
|
|
206
|
+
{ db, settings, bus },
|
|
207
|
+
{
|
|
208
|
+
accountId,
|
|
209
|
+
workspaceId,
|
|
210
|
+
sessionId: session.id,
|
|
211
|
+
pinnedSelfhosted: {
|
|
212
|
+
sandboxId: sandbox.id,
|
|
213
|
+
epoch: pointer.activeEpoch,
|
|
214
|
+
},
|
|
215
|
+
directRequest: { requestId, holderId },
|
|
216
|
+
},
|
|
217
|
+
established,
|
|
218
|
+
);
|
|
219
|
+
return await runEstablished(routed, null);
|
|
220
|
+
} catch (error) {
|
|
221
|
+
throw mapChannelAError(error);
|
|
222
|
+
} finally {
|
|
223
|
+
await dropEstablishedHandle(established);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
110
227
|
const release = async (): Promise<void> => {
|
|
111
228
|
await releaseLeaseHolder(db, {
|
|
112
229
|
accountId,
|
|
@@ -150,36 +267,6 @@ export async function withChannelA<T>(
|
|
|
150
267
|
|
|
151
268
|
try {
|
|
152
269
|
const envelope = await getSandboxSessionEnvelope(db, workspaceId, session.id);
|
|
153
|
-
// The STABLE run-environment a COLD box must be created with so a later worker
|
|
154
|
-
// turn's agent-manifest apply finds an EMPTY env delta (config base + git
|
|
155
|
-
// identity + decrypted workspace env + HOME + — for a repo-attached session —
|
|
156
|
-
// the stable git-auth pointers the turn declares). Only the rotating token
|
|
157
|
-
// VALUE stays off (it lives in the box file the clone hook seeds). Keyed off
|
|
158
|
-
// the SESSION's backend (the establish below passes backendOverride:
|
|
159
|
-
// session.sandboxBackend, and HOME/token-file/askpass are backend-derived),
|
|
160
|
-
// NOT the deployment default — mirrors sessionAttachEnvironment.
|
|
161
|
-
const workspaceEnvironment = await loadWorkspaceEnvironmentForRun(
|
|
162
|
-
db,
|
|
163
|
-
settings,
|
|
164
|
-
workspaceId,
|
|
165
|
-
session.environmentId,
|
|
166
|
-
);
|
|
167
|
-
const settingsForSession =
|
|
168
|
-
session.sandboxBackend !== settings.sandboxBackend
|
|
169
|
-
? { ...settings, sandboxBackend: session.sandboxBackend }
|
|
170
|
-
: settings;
|
|
171
|
-
const environment = stableSandboxEnvironmentForRun(
|
|
172
|
-
settingsForSession,
|
|
173
|
-
workspaceEnvironment?.values ?? {},
|
|
174
|
-
{ workspaceId },
|
|
175
|
-
);
|
|
176
|
-
if (hasGitCredentialRepositorySelection(session.resources)) {
|
|
177
|
-
applyGitAuthPointerEnvironment(
|
|
178
|
-
environment,
|
|
179
|
-
hasGitHubRepositorySelection(session.resources) ? githubAppBotIdentity(settings) : null,
|
|
180
|
-
);
|
|
181
|
-
}
|
|
182
|
-
|
|
183
270
|
if (acquired.role === "spawner") {
|
|
184
271
|
// We won the cold->warming CAS: establish the box from the envelope, then
|
|
185
272
|
// commit warm. The established handle IS our live handle for the op.
|
|
@@ -261,22 +348,10 @@ export async function withChannelA<T>(
|
|
|
261
348
|
}
|
|
262
349
|
}
|
|
263
350
|
|
|
264
|
-
const emit = async (events: { type: string; payload: unknown }[]): Promise<void> => {
|
|
265
|
-
await appendAndPublishEvents(
|
|
266
|
-
db,
|
|
267
|
-
bus,
|
|
268
|
-
workspaceId,
|
|
269
|
-
session.id,
|
|
270
|
-
// SessionEventType is a string enum at the contract; the producer parses
|
|
271
|
-
// the payload, so this cast is the same shape the worker emits.
|
|
272
|
-
events.map((e) => ({ type: e.type as never, payload: e.payload })),
|
|
273
|
-
);
|
|
274
|
-
};
|
|
275
|
-
|
|
276
351
|
// Route every call through the same proxy, even when hot-swap is disabled:
|
|
277
352
|
// routing may be dormant, but its direct mutation admission is mandatory for
|
|
278
353
|
// every persistable provider write.
|
|
279
|
-
const
|
|
354
|
+
const routed = wrapChannelABoxWithRouting(
|
|
280
355
|
{ db, settings, bus },
|
|
281
356
|
{
|
|
282
357
|
accountId,
|
|
@@ -291,22 +366,8 @@ export async function withChannelA<T>(
|
|
|
291
366
|
directRequest: { requestId, holderId },
|
|
292
367
|
},
|
|
293
368
|
established,
|
|
294
|
-
)
|
|
295
|
-
|
|
296
|
-
const scopedSession = environment.OPENGENI_TOOLSPACE_TOKEN_FILE
|
|
297
|
-
? withToolspaceTokenSession(
|
|
298
|
-
credentialSession,
|
|
299
|
-
toolspaceTokenFileFromEnvironment(environment, session.id),
|
|
300
|
-
)
|
|
301
|
-
: credentialSession;
|
|
302
|
-
|
|
303
|
-
const service = new SandboxChannelAService({
|
|
304
|
-
session: scopedSession as ChannelASession,
|
|
305
|
-
leaseEpoch: leaseSnapshot.leaseEpoch,
|
|
306
|
-
emit,
|
|
307
|
-
});
|
|
308
|
-
|
|
309
|
-
return await fn({ service, lease: leaseSnapshot, routingSession: routedSession, requestId });
|
|
369
|
+
);
|
|
370
|
+
return await runEstablished(routed, leaseSnapshot);
|
|
310
371
|
} catch (error) {
|
|
311
372
|
throw mapChannelAError(error);
|
|
312
373
|
} finally {
|