@telorun/runner-core 0.7.0 → 0.8.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/backend.d.ts +1 -1
- package/dist/contract.d.ts +5 -12
- package/dist/contract.d.ts.map +1 -1
- package/dist/contract.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/routes/apps.d.ts +29 -0
- package/dist/routes/apps.d.ts.map +1 -0
- package/dist/routes/apps.js +64 -0
- package/dist/routes/apps.js.map +1 -0
- package/dist/routes/session-start.d.ts +59 -0
- package/dist/routes/session-start.d.ts.map +1 -0
- package/dist/routes/session-start.js +129 -0
- package/dist/routes/session-start.js.map +1 -0
- package/dist/routes/sessions.d.ts +2 -7
- package/dist/routes/sessions.d.ts.map +1 -1
- package/dist/routes/sessions.js +28 -175
- package/dist/routes/sessions.js.map +1 -1
- package/dist/server.d.ts +2 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +8 -1
- package/dist/server.js.map +1 -1
- package/package.json +2 -2
- package/src/backend.ts +1 -1
- package/src/contract.ts +5 -12
- package/src/index.ts +1 -0
- package/src/routes/apps.ts +106 -0
- package/src/routes/session-start.ts +174 -0
- package/src/routes/sessions.ts +34 -196
- package/src/server.ts +12 -2
package/dist/routes/sessions.js
CHANGED
|
@@ -1,16 +1,10 @@
|
|
|
1
|
-
import { isEventFrame } from "@telorun/debug-wire";
|
|
2
|
-
import { ACCEPTED_TERMS_HEADER, SessionStartError, } from "../contract.js";
|
|
3
1
|
import { BundlePathError, normalizeBundlePath } from "../session/bundle-path.js";
|
|
4
|
-
import {
|
|
5
|
-
import { SessionLimitError } from "../session/registry.js";
|
|
2
|
+
import { enforceTerms, portsSchema, startWorkloadSession } from "./session-start.js";
|
|
6
3
|
import { streamSessionEvents } from "../sse/channel.js";
|
|
7
|
-
// `bundle`/`config` are schema-optional because an `app` session needs neither;
|
|
8
|
-
// the route enforces their presence for regular bundle sessions.
|
|
9
4
|
const startBodySchema = {
|
|
10
5
|
type: "object",
|
|
11
|
-
required: ["env"],
|
|
6
|
+
required: ["bundle", "env", "config"],
|
|
12
7
|
properties: {
|
|
13
|
-
app: { type: "string", minLength: 1 },
|
|
14
8
|
bundle: {
|
|
15
9
|
type: "object",
|
|
16
10
|
required: ["entryRelativePath", "files"],
|
|
@@ -33,17 +27,7 @@ const startBodySchema = {
|
|
|
33
27
|
type: "object",
|
|
34
28
|
additionalProperties: { type: "string" },
|
|
35
29
|
},
|
|
36
|
-
ports:
|
|
37
|
-
type: "array",
|
|
38
|
-
items: {
|
|
39
|
-
type: "object",
|
|
40
|
-
required: ["port", "protocol"],
|
|
41
|
-
properties: {
|
|
42
|
-
port: { type: "integer", minimum: 1, maximum: 65535 },
|
|
43
|
-
protocol: { type: "string", enum: ["tcp", "udp"] },
|
|
44
|
-
},
|
|
45
|
-
},
|
|
46
|
-
},
|
|
30
|
+
ports: portsSchema,
|
|
47
31
|
config: {
|
|
48
32
|
type: "object",
|
|
49
33
|
required: ["image", "pullPolicy"],
|
|
@@ -59,17 +43,8 @@ const startBodySchema = {
|
|
|
59
43
|
export function sessionsRoute(deps) {
|
|
60
44
|
return async (app) => {
|
|
61
45
|
app.post("/v1/sessions", { schema: { body: startBodySchema } }, async (req, reply) => {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
// the current terms version.
|
|
65
|
-
if (deps.terms) {
|
|
66
|
-
const raw = req.headers[ACCEPTED_TERMS_HEADER];
|
|
67
|
-
const accepted = Array.isArray(raw) ? raw[0] : raw;
|
|
68
|
-
if (accepted !== deps.terms.version) {
|
|
69
|
-
reply.code(428).send({ error: "terms_required", terms: deps.terms });
|
|
70
|
-
return;
|
|
71
|
-
}
|
|
72
|
-
}
|
|
46
|
+
if (!enforceTerms(req, reply, deps.terms))
|
|
47
|
+
return;
|
|
73
48
|
return startSession(app, deps, req.body, reply);
|
|
74
49
|
});
|
|
75
50
|
app.get("/v1/sessions/:id", async (req, reply) => {
|
|
@@ -114,162 +89,40 @@ export function sessionsRoute(deps) {
|
|
|
114
89
|
};
|
|
115
90
|
}
|
|
116
91
|
async function startSession(app, deps, body, reply) {
|
|
117
|
-
const sessionId = generateSessionId();
|
|
118
|
-
// App sessions launch an operator-predefined image by name: the catalog
|
|
119
|
-
// resolves the image and operator env server-side, so the client can neither
|
|
120
|
-
// pick the image nor reach the secrets — the catalog IS the gate, and an
|
|
121
|
-
// unknown name is rejected here.
|
|
122
|
-
const appEntry = body.app === undefined ? undefined : deps.apps?.[body.app];
|
|
123
|
-
if (body.app !== undefined && !appEntry) {
|
|
124
|
-
const offered = Object.keys(deps.apps ?? {});
|
|
125
|
-
reply.code(400).send({
|
|
126
|
-
error: "unknown_app",
|
|
127
|
-
message: `app '${body.app}' is not offered by this runner` +
|
|
128
|
-
(offered.length > 0
|
|
129
|
-
? ` — offered apps: ${offered.join(", ")}`
|
|
130
|
-
: " — it offers no predefined applications") +
|
|
131
|
-
" (see /v1/capabilities).",
|
|
132
|
-
});
|
|
133
|
-
return;
|
|
134
|
-
}
|
|
135
|
-
if (!appEntry && (!body.bundle || !body.config)) {
|
|
136
|
-
reply.code(400).send({
|
|
137
|
-
error: "invalid_request",
|
|
138
|
-
message: "'bundle' and 'config' are required unless launching a predefined app via 'app'.",
|
|
139
|
-
});
|
|
140
|
-
return;
|
|
141
|
-
}
|
|
142
|
-
let bundle;
|
|
143
92
|
let entryRelative;
|
|
144
|
-
let config;
|
|
145
|
-
if (appEntry) {
|
|
146
|
-
// Self-contained image — no bundle to deliver; the entry path is an unused
|
|
147
|
-
// placeholder so the backend spec stays total.
|
|
148
|
-
bundle = { entryRelativePath: "telo.yaml", files: [] };
|
|
149
|
-
entryRelative = bundle.entryRelativePath;
|
|
150
|
-
config = { image: appEntry.image, pullPolicy: appEntry.pullPolicy };
|
|
151
|
-
}
|
|
152
|
-
else {
|
|
153
|
-
bundle = body.bundle;
|
|
154
|
-
config = body.config;
|
|
155
|
-
try {
|
|
156
|
-
// Traversal guard for the entry path and every bundle file — a `../foo`
|
|
157
|
-
// would let the workload read or execute paths outside its session dir.
|
|
158
|
-
// Validated here (backend-neutral) so a bad path is a 400, not a backend
|
|
159
|
-
// 500, regardless of how the backend ultimately delivers the bundle.
|
|
160
|
-
entryRelative = normalizeBundlePath(bundle.entryRelativePath);
|
|
161
|
-
for (const file of bundle.files)
|
|
162
|
-
normalizeBundlePath(file.relativePath);
|
|
163
|
-
}
|
|
164
|
-
catch (err) {
|
|
165
|
-
if (err instanceof BundlePathError) {
|
|
166
|
-
reply.code(400).send({ error: "invalid_bundle", message: err.message });
|
|
167
|
-
return;
|
|
168
|
-
}
|
|
169
|
-
throw err;
|
|
170
|
-
}
|
|
171
|
-
// Backend config gate (e.g. an image allowlist). The advertised capabilities
|
|
172
|
-
// constrain the editor; this enforces the same against any client. App
|
|
173
|
-
// sessions skip it — their image comes from the catalog, not the client.
|
|
174
|
-
if (deps.validateConfig) {
|
|
175
|
-
const message = deps.validateConfig(config);
|
|
176
|
-
if (message) {
|
|
177
|
-
reply.code(400).send({ error: "invalid_config", message });
|
|
178
|
-
return;
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
let entry;
|
|
183
93
|
try {
|
|
184
|
-
entry
|
|
94
|
+
// Traversal guard for the entry path and every bundle file — a `../foo`
|
|
95
|
+
// would let the workload read or execute paths outside its session dir.
|
|
96
|
+
// Validated here (backend-neutral) so a bad path is a 400, not a backend
|
|
97
|
+
// 500, regardless of how the backend ultimately delivers the bundle.
|
|
98
|
+
entryRelative = normalizeBundlePath(body.bundle.entryRelativePath);
|
|
99
|
+
for (const file of body.bundle.files)
|
|
100
|
+
normalizeBundlePath(file.relativePath);
|
|
185
101
|
}
|
|
186
102
|
catch (err) {
|
|
187
|
-
if (err instanceof
|
|
188
|
-
reply.code(
|
|
103
|
+
if (err instanceof BundlePathError) {
|
|
104
|
+
reply.code(400).send({ error: "invalid_bundle", message: err.message });
|
|
189
105
|
return;
|
|
190
106
|
}
|
|
191
107
|
throw err;
|
|
192
108
|
}
|
|
193
|
-
//
|
|
194
|
-
//
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
109
|
+
// Backend config gate (e.g. an image allowlist). The advertised capabilities
|
|
110
|
+
// constrain the editor; this enforces the same against any client.
|
|
111
|
+
if (deps.validateConfig) {
|
|
112
|
+
const message = deps.validateConfig(body.config);
|
|
113
|
+
if (message) {
|
|
114
|
+
reply.code(400).send({ error: "invalid_config", message });
|
|
115
|
+
return;
|
|
200
116
|
}
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
// override) > runner's own default. Trim client-supplied URLs so stray
|
|
205
|
-
// whitespace from an editor input doesn't flow into the workload.
|
|
206
|
-
const configRegistryUrl = config.registryUrl?.trim() || undefined;
|
|
207
|
-
const registryUrl = configRegistryUrl ?? deps.defaultRegistryUrl;
|
|
208
|
-
const sessionEnv = registryUrl && !("TELO_REGISTRY_URL" in clientEnv)
|
|
209
|
-
? { ...clientEnv, TELO_REGISTRY_URL: registryUrl }
|
|
210
|
-
: clientEnv;
|
|
211
|
-
// Respond as soon as the session is registered — BEFORE the backend starts.
|
|
212
|
-
// `backend.start()` now spans the on-cluster image build and pod bring-up,
|
|
213
|
-
// which can take seconds-to-minutes; awaiting it here would hide the event
|
|
214
|
-
// stream until the workload is already up, so the client never sees build /
|
|
215
|
-
// provision / boot progress live. Returning the streamUrl first lets the
|
|
216
|
-
// client connect immediately; start runs in the background and its progress,
|
|
217
|
-
// output, and terminal status flow over the stream.
|
|
218
|
-
reply.code(201).send({
|
|
219
|
-
sessionId,
|
|
220
|
-
streamUrl: `/v1/sessions/${sessionId}/events`,
|
|
221
|
-
createdAt: entry.createdAt.toISOString(),
|
|
222
|
-
});
|
|
223
|
-
deps.backend
|
|
224
|
-
.start({
|
|
225
|
-
sessionId,
|
|
226
|
-
bundle,
|
|
117
|
+
}
|
|
118
|
+
return startWorkloadSession(app, deps, {
|
|
119
|
+
bundle: body.bundle,
|
|
227
120
|
entryRelativePath: entryRelative,
|
|
228
|
-
env:
|
|
121
|
+
env: body.env,
|
|
229
122
|
ports: body.ports ?? [],
|
|
230
|
-
config,
|
|
231
|
-
selfContained:
|
|
123
|
+
config: body.config,
|
|
124
|
+
selfContained: false,
|
|
232
125
|
inspect: body.inspect ?? false,
|
|
233
|
-
|
|
234
|
-
onProgress: (phase, message, done) => deps.registry.emit(sessionId, { type: "progress", phase, message, done }),
|
|
235
|
-
onOutput: (chunk) => deps.registry.pushBytes(sessionId, chunk),
|
|
236
|
-
// Relay only kernel *event* frames to the client. stdout/stderr already
|
|
237
|
-
// arrive over the byte channel (onOutput), so forwarding log frames would
|
|
238
|
-
// double the traffic and let log spam evict lifecycle events from the
|
|
239
|
-
// byte-capped replay buffer. The editor discards relayed logs anyway.
|
|
240
|
-
onDebug: (frame) => {
|
|
241
|
-
if (isEventFrame(frame))
|
|
242
|
-
deps.registry.emit(sessionId, { type: "debug", frame });
|
|
243
|
-
},
|
|
244
|
-
onReachability: (port, state) => deps.registry.emit(sessionId, { type: "reachability", port, state }),
|
|
245
|
-
isUserStopped: () => entry.userStopped,
|
|
246
|
-
})
|
|
247
|
-
.then(async (session) => {
|
|
248
|
-
entry.session = session;
|
|
249
|
-
// Pre-start DELETE race: a DELETE received during backend.start (e.g.
|
|
250
|
-
// while an image build was running) can't stop a workload that didn't
|
|
251
|
-
// exist yet — it set userStopped and returned 204. Now that the workload
|
|
252
|
-
// is live, honor the earlier DELETE.
|
|
253
|
-
if (entry.userStopped) {
|
|
254
|
-
try {
|
|
255
|
-
await session.stop();
|
|
256
|
-
}
|
|
257
|
-
catch (err) {
|
|
258
|
-
app.log.warn({ err, sessionId }, "failed to stop after race with pre-start DELETE");
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
})
|
|
262
|
-
.catch((err) => {
|
|
263
|
-
// The 201 is already sent, so a start failure surfaces as a terminal
|
|
264
|
-
// `failed` status on the stream (the registry schedules eviction on a
|
|
265
|
-
// terminal status; the SSE channel delivers it, then closes).
|
|
266
|
-
const message = err instanceof SessionStartError
|
|
267
|
-
? `${err.stage}: ${err.message}`
|
|
268
|
-
: err instanceof Error
|
|
269
|
-
? err.message
|
|
270
|
-
: String(err);
|
|
271
|
-
app.log.error({ err, sessionId }, "session start failed");
|
|
272
|
-
deps.registry.emit(sessionId, { type: "status", status: { kind: "failed", message } });
|
|
273
|
-
});
|
|
126
|
+
}, reply);
|
|
274
127
|
}
|
|
275
128
|
//# sourceMappingURL=sessions.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sessions.js","sourceRoot":"","sources":["../../src/routes/sessions.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"sessions.js","sourceRoot":"","sources":["../../src/routes/sessions.ts"],"names":[],"mappings":"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"}
|
package/dist/server.d.ts
CHANGED
|
@@ -21,7 +21,8 @@ export interface ServerDeps {
|
|
|
21
21
|
validateConfig?: (config: SessionConfig) => string | undefined;
|
|
22
22
|
/** Operator-predefined applications launchable by name (usually
|
|
23
23
|
* `loadResolvedApps(process.env)`). Advertised on /v1/capabilities as
|
|
24
|
-
* `apps` descriptors;
|
|
24
|
+
* `apps` descriptors; sessions of them are created via
|
|
25
|
+
* `POST /v1/apps/:name/sessions`. */
|
|
25
26
|
apps?: Record<string, ResolvedRunnerApp>;
|
|
26
27
|
registry?: SessionRegistry;
|
|
27
28
|
}
|
package/dist/server.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA,OAAgB,EAAE,KAAK,iBAAiB,EAAE,KAAK,eAAe,EAAE,MAAM,SAAS,CAAC;AAIhF,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAClD,OAAO,KAAK,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AACvE,OAAO,KAAK,EAAuB,kBAAkB,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA,OAAgB,EAAE,KAAK,iBAAiB,EAAE,KAAK,eAAe,EAAE,MAAM,SAAS,CAAC;AAIhF,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAClD,OAAO,KAAK,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AACvE,OAAO,KAAK,EAAuB,kBAAkB,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAO5F,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAExD,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE,aAAa,CAAC;IACvB,MAAM,EAAE,gBAAgB,CAAC;IACzB,qEAAqE;IACrE,OAAO,EAAE,MAAM,CAAC;IAChB;;;4EAGwE;IACxE,YAAY,EAAE,kBAAkB,GAAG,CAAC,MAAM,kBAAkB,CAAC,CAAC;IAC9D,+EAA+E;IAC/E,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B;;0EAEsE;IACtE,cAAc,CAAC,EAAE,CAAC,MAAM,EAAE,aAAa,KAAK,MAAM,GAAG,SAAS,CAAC;IAC/D;;;0CAGsC;IACtC,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,iBAAiB,CAAC,CAAC;IACzC,QAAQ,CAAC,EAAE,eAAe,CAAC;CAC5B;AAED,MAAM,WAAW,YAAY;IAC3B,GAAG,EAAE,eAAe,CAAC;IACrB,QAAQ,EAAE,eAAe,CAAC;CAC3B;AAED,wBAAsB,WAAW,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,YAAY,CAAC,CAoEzE;AAED;;;;GAIG;AACH,wBAAsB,eAAe,CACnC,QAAQ,EAAE,eAAe,EACzB,GAAG,EAAE,IAAI,CAAC,iBAAiB,EAAE,MAAM,GAAG,MAAM,CAAC,GAC5C,OAAO,CAAC,IAAI,CAAC,CAcf"}
|
package/dist/server.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import Fastify from "fastify";
|
|
2
2
|
import cors from "@fastify/cors";
|
|
3
3
|
import websocket from "@fastify/websocket";
|
|
4
|
+
import { appsRoute } from "./routes/apps.js";
|
|
4
5
|
import { capabilitiesRoute } from "./routes/capabilities.js";
|
|
5
6
|
import { healthRoute } from "./routes/health.js";
|
|
6
7
|
import { ioRoute } from "./routes/io.js";
|
|
@@ -46,7 +47,13 @@ export async function buildServer(deps) {
|
|
|
46
47
|
defaultRegistryUrl: deps.defaultRegistryUrl,
|
|
47
48
|
validateConfig: deps.validateConfig,
|
|
48
49
|
// The capabilities document is the single source of the runner's terms;
|
|
49
|
-
//
|
|
50
|
+
// every session-creating route enforces what /v1/capabilities advertises.
|
|
51
|
+
terms: capabilitiesValue.terms,
|
|
52
|
+
}));
|
|
53
|
+
await app.register(appsRoute({
|
|
54
|
+
backend: deps.backend,
|
|
55
|
+
registry,
|
|
56
|
+
defaultRegistryUrl: deps.defaultRegistryUrl,
|
|
50
57
|
terms: capabilitiesValue.terms,
|
|
51
58
|
apps: deps.apps,
|
|
52
59
|
}));
|
package/dist/server.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA,OAAO,OAAyD,MAAM,SAAS,CAAC;AAChF,OAAO,IAAI,MAAM,eAAe,CAAC;AACjC,OAAO,SAAS,MAAM,oBAAoB,CAAC;AAK3C,OAAO,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAC7D,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACjD,OAAO,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAC;AACzC,OAAO,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAC/C,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AACrD,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;
|
|
1
|
+
{"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA,OAAO,OAAyD,MAAM,SAAS,CAAC;AAChF,OAAO,IAAI,MAAM,eAAe,CAAC;AACjC,OAAO,SAAS,MAAM,oBAAoB,CAAC;AAK3C,OAAO,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAC7C,OAAO,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAC7D,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACjD,OAAO,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAC;AACzC,OAAO,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAC/C,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AACrD,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AA+BxD,MAAM,CAAC,KAAK,UAAU,WAAW,CAAC,IAAgB;IAChD,MAAM,GAAG,GAAG,OAAO,CAAC;QAClB,MAAM,EAAE,EAAE,KAAK,EAAE,IAAI,CAAC,MAAM,CAAC,QAAQ,EAAE;KACxC,CAAC,CAAC;IAEH,2EAA2E;IAC3E,6EAA6E;IAC7E,mEAAmE;IACnE,MAAM,GAAG,CAAC,QAAQ,CAAC,IAAI,EAAE;QACvB,MAAM,EAAE,IAAI,CAAC,MAAM,CAAC,WAAW;QAC/B,OAAO,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,SAAS,CAAC;KAC9C,CAAC,CAAC;IAEH,MAAM,GAAG,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC;IAE9B,MAAM,QAAQ,GACZ,IAAI,CAAC,QAAQ;QACb,IAAI,eAAe,CAAC;YAClB,WAAW,EAAE,IAAI,CAAC,MAAM,CAAC,WAAW;YACpC,SAAS,EAAE,IAAI,CAAC,MAAM,CAAC,SAAS;YAChC,iBAAiB,EAAE,IAAI,CAAC,MAAM,CAAC,iBAAiB;SACjD,CAAC,CAAC;IAEL,6EAA6E;IAC7E,0EAA0E;IAC1E,4CAA4C;IAC5C,MAAM,cAAc,GAA0B,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC,GAAG,CAC9E,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,WAAW,EAAE,EAAE,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,WAAW,EAAE,CAAC,CACjE,CAAC;IACF,MAAM,QAAQ,GAAG,CAAC,IAAwB,EAAsB,EAAE,CAChE,cAAc,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,GAAG,IAAI,EAAE,IAAI,EAAE,cAAc,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;IACvE,MAAM,kBAAkB,GACtB,OAAO,IAAI,CAAC,YAAY,KAAK,UAAU;QACrC,CAAC,CAAC,GAAG,EAAE,CAAC,QAAQ,CAAE,IAAI,CAAC,YAAyC,EAAE,CAAC;QACnE,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC;IAElC,+EAA+E;IAC/E,kFAAkF;IAClF,MAAM,iBAAiB,GACrB,OAAO,kBAAkB,KAAK,UAAU,CAAC,CAAC,CAAC,kBAAkB,EAAE,CAAC,CAAC,CAAC,kBAAkB,CAAC;IAEvF,MAAM,GAAG,CAAC,QAAQ,CAAC,WAAW,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC;IAC9C,MAAM,GAAG,CAAC,QAAQ,CAAC,iBAAiB,CAAC,kBAAkB,CAAC,CAAC,CAAC;IAC1D,MAAM,GAAG,CAAC,QAAQ,CAAC,UAAU,CAAC,EAAE,OAAO,EAAE,IAAI,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC;IAC1D,MAAM,GAAG,CAAC,QAAQ,CAChB,aAAa,CAAC;QACZ,OAAO,EAAE,IAAI,CAAC,OAAO;QACrB,QAAQ;QACR,WAAW,EAAE,IAAI,CAAC,MAAM,CAAC,WAAW;QACpC,kBAAkB,EAAE,IAAI,CAAC,kBAAkB;QAC3C,cAAc,EAAE,IAAI,CAAC,cAAc;QACnC,wEAAwE;QACxE,0EAA0E;QAC1E,KAAK,EAAE,iBAAiB,CAAC,KAAK;KAC/B,CAAC,CACH,CAAC;IACF,MAAM,GAAG,CAAC,QAAQ,CAChB,SAAS,CAAC;QACR,OAAO,EAAE,IAAI,CAAC,OAAO;QACrB,QAAQ;QACR,kBAAkB,EAAE,IAAI,CAAC,kBAAkB;QAC3C,KAAK,EAAE,iBAAiB,CAAC,KAAK;QAC9B,IAAI,EAAE,IAAI,CAAC,IAAI;KAChB,CAAC,CACH,CAAC;IACF,MAAM,GAAG,CAAC,QAAQ,CAAC,OAAO,CAAC,EAAE,QAAQ,EAAE,WAAW,EAAE,IAAI,CAAC,MAAM,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC;IAEhF,OAAO,EAAE,GAAG,EAAE,QAAQ,EAAE,CAAC;AAC3B,CAAC;AAED;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CACnC,QAAyB,EACzB,GAA6C;IAE7C,MAAM,IAAI,GAAG,QAAQ,CAAC,IAAI,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,KAAK,IAAI,IAAI,CAAC,CAAC,QAAQ,KAAK,IAAI,CAAC,CAAC;IACtF,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO;IAC9B,GAAG,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,IAAI,CAAC,MAAM,EAAE,EAAE,wCAAwC,CAAC,CAAC;IAC3E,MAAM,OAAO,CAAC,GAAG,CACf,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,KAAK,EAAE,EAAE;QACvB,KAAK,CAAC,WAAW,GAAG,IAAI,CAAC;QACzB,IAAI,CAAC;YACH,MAAM,KAAK,CAAC,OAAO,EAAE,IAAI,EAAE,CAAC;QAC9B,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,GAAG,CAAC,IAAI,CAAC,EAAE,GAAG,EAAE,SAAS,EAAE,KAAK,CAAC,SAAS,EAAE,EAAE,wCAAwC,CAAC,CAAC;QAC1F,CAAC;IACH,CAAC,CAAC,CACH,CAAC;AACJ,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@telorun/runner-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.1",
|
|
4
4
|
"description": "Backend-neutral core for Telo runners: the /v1 session contract, the RunnerBackend interface, the session registry, SSE/byte ring buffers, and bundle handling. Shared by docker-runner and k8s-runner.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"telo",
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"@fastify/websocket": "^11.0.1",
|
|
32
32
|
"fastify": "^5.7.2",
|
|
33
33
|
"yaml": "^2.8.3",
|
|
34
|
-
"@telorun/debug-wire": "0.
|
|
34
|
+
"@telorun/debug-wire": "0.4.0"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
37
|
"@types/node": "^20.0.0",
|
package/src/backend.ts
CHANGED
|
@@ -48,7 +48,7 @@ export interface BackendStartSpec {
|
|
|
48
48
|
env: Record<string, string>;
|
|
49
49
|
ports: PortMapping[];
|
|
50
50
|
config: SessionConfig;
|
|
51
|
-
/** True for an operator-predefined app session (`
|
|
51
|
+
/** True for an operator-predefined app session (`POST /v1/apps/:name/sessions`):
|
|
52
52
|
* `config.image` is self-contained (app + controllers baked in), so the
|
|
53
53
|
* backend runs the image's own entrypoint and stages no bundle — `bundle`
|
|
54
54
|
* is an empty placeholder. */
|
package/src/contract.ts
CHANGED
|
@@ -30,8 +30,8 @@ export interface RunnerCapabilities {
|
|
|
30
30
|
* matching `terms.version`. The editor surfaces it and records acceptance. */
|
|
31
31
|
terms?: RunnerTerms;
|
|
32
32
|
/** Operator-predefined applications this runner can launch by name
|
|
33
|
-
* (`
|
|
34
|
-
* hide the corresponding entry points. */
|
|
33
|
+
* (`POST /v1/apps/:name/sessions`). Absent/empty when none are offered —
|
|
34
|
+
* clients hide the corresponding entry points. */
|
|
35
35
|
apps?: RunnerAppDescriptor[];
|
|
36
36
|
}
|
|
37
37
|
|
|
@@ -59,7 +59,7 @@ export interface RunnerFeatures {
|
|
|
59
59
|
* secrets. Which apps exist is pure operator configuration (`RUNNER_APPS`);
|
|
60
60
|
* the runner has no built-in knowledge of any specific app. */
|
|
61
61
|
export interface RunnerAppDescriptor {
|
|
62
|
-
/** Wire id
|
|
62
|
+
/** Wire id: an app session is created via `POST /v1/apps/<name>/sessions`. */
|
|
63
63
|
name: string;
|
|
64
64
|
title?: string;
|
|
65
65
|
description?: string;
|
|
@@ -119,17 +119,10 @@ export interface RunnerEndpoint {
|
|
|
119
119
|
}
|
|
120
120
|
|
|
121
121
|
export interface StartSessionRequest {
|
|
122
|
-
|
|
123
|
-
* {@link RunnerAppDescriptor}) instead of a client bundle. The runner
|
|
124
|
-
* resolves the image and injects the app's operator env server-side;
|
|
125
|
-
* `bundle` and `config.image` are ignored. */
|
|
126
|
-
app?: string;
|
|
127
|
-
/** Required unless `app` is set. */
|
|
128
|
-
bundle?: RunBundle;
|
|
122
|
+
bundle: RunBundle;
|
|
129
123
|
env: Record<string, string>;
|
|
130
124
|
ports?: PortMapping[];
|
|
131
|
-
|
|
132
|
-
config?: SessionConfig;
|
|
125
|
+
config: SessionConfig;
|
|
133
126
|
/** Request the kernel debug stream. When true the runner launches the
|
|
134
127
|
* workload with `--inspect`, subscribes to the in-workload inspect endpoint
|
|
135
128
|
* (reachable only by the runner — never published outward), and relays each
|
package/src/index.ts
CHANGED
|
@@ -28,6 +28,7 @@ export { healthRoute } from "./routes/health.js";
|
|
|
28
28
|
export { capabilitiesRoute } from "./routes/capabilities.js";
|
|
29
29
|
export { probeRoute, type ProbeRouteDeps } from "./routes/probe.js";
|
|
30
30
|
export { sessionsRoute, type SessionsRouteDeps } from "./routes/sessions.js";
|
|
31
|
+
export { appsRoute, type AppsRouteDeps } from "./routes/apps.js";
|
|
31
32
|
export { ioRoute, type IoRouteDeps } from "./routes/io.js";
|
|
32
33
|
export { relayDebugStream, type DebugRelayOptions } from "./debug/relay.js";
|
|
33
34
|
export {
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import type { FastifyInstance, FastifyPluginAsync } from "fastify";
|
|
2
|
+
|
|
3
|
+
import type { RunnerBackend } from "../backend.js";
|
|
4
|
+
import type { ResolvedRunnerApp } from "../config.js";
|
|
5
|
+
import type { PortMapping, RunnerTerms } from "../contract.js";
|
|
6
|
+
import type { SessionRegistry } from "../session/registry.js";
|
|
7
|
+
import { enforceTerms, portsSchema, startWorkloadSession } from "./session-start.js";
|
|
8
|
+
|
|
9
|
+
export interface AppsRouteDeps {
|
|
10
|
+
backend: RunnerBackend;
|
|
11
|
+
registry: SessionRegistry;
|
|
12
|
+
/** The runner's own default registry URL, surfaced to the workload as
|
|
13
|
+
* TELO_REGISTRY_URL when the request doesn't override it. */
|
|
14
|
+
defaultRegistryUrl?: string;
|
|
15
|
+
/** Terms gate shared with `POST /v1/sessions` — app sessions execute on the
|
|
16
|
+
* operator's infrastructure just like runs, so they ride the same 428. */
|
|
17
|
+
terms?: RunnerTerms;
|
|
18
|
+
/** Operator-predefined applications launchable by name. The catalog is the
|
|
19
|
+
* whole gate: the client picks a name, never an image, and the operator env
|
|
20
|
+
* is injected server-side. */
|
|
21
|
+
apps?: Record<string, ResolvedRunnerApp>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface StartAppSessionBody {
|
|
25
|
+
env?: Record<string, string>;
|
|
26
|
+
ports?: PortMapping[];
|
|
27
|
+
inspect?: boolean;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const startAppBodySchema = {
|
|
31
|
+
type: "object",
|
|
32
|
+
properties: {
|
|
33
|
+
env: {
|
|
34
|
+
type: "object",
|
|
35
|
+
additionalProperties: { type: "string" },
|
|
36
|
+
},
|
|
37
|
+
ports: portsSchema,
|
|
38
|
+
inspect: { type: "boolean" },
|
|
39
|
+
},
|
|
40
|
+
} as const;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Creation door for operator-predefined app sessions. A client launches an app
|
|
44
|
+
* by name; the runner resolves the image and injects the app's operator env
|
|
45
|
+
* from the catalog (`RUNNER_APPS`). The created session lives in the SAME
|
|
46
|
+
* session collection as bundle sessions — the 201 carries the shared
|
|
47
|
+
* `/v1/sessions/:id/events` stream URL, and status/DELETE/io are served by the
|
|
48
|
+
* sessions routes.
|
|
49
|
+
*/
|
|
50
|
+
export function appsRoute(deps: AppsRouteDeps): FastifyPluginAsync {
|
|
51
|
+
return async (app: FastifyInstance) => {
|
|
52
|
+
app.post<{ Params: { name: string }; Body: StartAppSessionBody }>(
|
|
53
|
+
"/v1/apps/:name/sessions",
|
|
54
|
+
{ schema: { body: startAppBodySchema } },
|
|
55
|
+
async (req, reply) => {
|
|
56
|
+
// Resolve the app before the terms gate: an unknown name is a 404
|
|
57
|
+
// regardless of terms (there's no workload to gate), and the catalog is
|
|
58
|
+
// already public on /v1/capabilities, so this leaks nothing.
|
|
59
|
+
const appEntry = deps.apps?.[req.params.name];
|
|
60
|
+
if (!appEntry) {
|
|
61
|
+
const offered = Object.keys(deps.apps ?? {});
|
|
62
|
+
reply.code(404).send({
|
|
63
|
+
error: "unknown_app",
|
|
64
|
+
message:
|
|
65
|
+
`app '${req.params.name}' is not offered by this runner` +
|
|
66
|
+
(offered.length > 0
|
|
67
|
+
? ` — offered apps: ${offered.join(", ")}`
|
|
68
|
+
: " — it offers no predefined applications") +
|
|
69
|
+
" (see /v1/capabilities).",
|
|
70
|
+
});
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (!enforceTerms(req, reply, deps.terms)) return;
|
|
75
|
+
|
|
76
|
+
// Drop client-supplied values for any env key the catalog defines (a
|
|
77
|
+
// client must never override operator-held values, which include
|
|
78
|
+
// secrets), then inject the operator's values.
|
|
79
|
+
const clientEnv = req.body?.env ?? {};
|
|
80
|
+
const env = {
|
|
81
|
+
...Object.fromEntries(
|
|
82
|
+
Object.entries(clientEnv).filter(([key]) => !(key in appEntry.env)),
|
|
83
|
+
),
|
|
84
|
+
...appEntry.env,
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
return startWorkloadSession(
|
|
88
|
+
app,
|
|
89
|
+
deps,
|
|
90
|
+
{
|
|
91
|
+
// Self-contained image — no bundle to deliver; the entry path is an
|
|
92
|
+
// unused placeholder so the backend spec stays total.
|
|
93
|
+
bundle: { entryRelativePath: "telo.yaml", files: [] },
|
|
94
|
+
entryRelativePath: "telo.yaml",
|
|
95
|
+
env,
|
|
96
|
+
ports: req.body?.ports ?? [],
|
|
97
|
+
config: { image: appEntry.image, pullPolicy: appEntry.pullPolicy },
|
|
98
|
+
selfContained: true,
|
|
99
|
+
inspect: req.body?.inspect ?? false,
|
|
100
|
+
},
|
|
101
|
+
reply,
|
|
102
|
+
);
|
|
103
|
+
},
|
|
104
|
+
);
|
|
105
|
+
};
|
|
106
|
+
}
|