@telorun/runner-core 0.5.2 → 0.7.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/abortable-delay.d.ts +10 -0
- package/dist/abortable-delay.d.ts.map +1 -0
- package/dist/abortable-delay.js +26 -0
- package/dist/abortable-delay.js.map +1 -0
- package/dist/backend.d.ts +10 -1
- package/dist/backend.d.ts.map +1 -1
- package/dist/config.d.ts +43 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +77 -0
- package/dist/config.js.map +1 -1
- package/dist/contract.d.ts +34 -2
- package/dist/contract.d.ts.map +1 -1
- package/dist/contract.js.map +1 -1
- package/dist/debug/relay.d.ts.map +1 -1
- package/dist/debug/relay.js +3 -17
- package/dist/debug/relay.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/reachability.d.ts +37 -0
- package/dist/reachability.d.ts.map +1 -0
- package/dist/reachability.js +71 -0
- package/dist/reachability.js.map +1 -0
- package/dist/routes/sessions.d.ts +5 -0
- package/dist/routes/sessions.d.ts.map +1 -1
- package/dist/routes/sessions.js +83 -31
- package/dist/routes/sessions.js.map +1 -1
- package/dist/server.d.ts +7 -2
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +11 -2
- package/dist/server.js.map +1 -1
- package/package.json +1 -1
- package/src/abortable-delay.ts +24 -0
- package/src/app-catalog.test.ts +54 -0
- package/src/backend.ts +10 -0
- package/src/config.ts +116 -1
- package/src/contract.ts +33 -3
- package/src/debug/relay.ts +4 -17
- package/src/index.ts +4 -0
- package/src/reachability.test.ts +83 -0
- package/src/reachability.ts +106 -0
- package/src/routes/sessions.ts +93 -30
- package/src/server.ts +24 -5
package/dist/routes/sessions.js
CHANGED
|
@@ -4,10 +4,13 @@ import { BundlePathError, normalizeBundlePath } from "../session/bundle-path.js"
|
|
|
4
4
|
import { generateSessionId } from "../session/session-id.js";
|
|
5
5
|
import { SessionLimitError } from "../session/registry.js";
|
|
6
6
|
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.
|
|
7
9
|
const startBodySchema = {
|
|
8
10
|
type: "object",
|
|
9
|
-
required: ["
|
|
11
|
+
required: ["env"],
|
|
10
12
|
properties: {
|
|
13
|
+
app: { type: "string", minLength: 1 },
|
|
11
14
|
bundle: {
|
|
12
15
|
type: "object",
|
|
13
16
|
required: ["entryRelativePath", "files"],
|
|
@@ -112,30 +115,68 @@ export function sessionsRoute(deps) {
|
|
|
112
115
|
}
|
|
113
116
|
async function startSession(app, deps, body, reply) {
|
|
114
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;
|
|
115
143
|
let entryRelative;
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
//
|
|
119
|
-
//
|
|
120
|
-
|
|
121
|
-
entryRelative =
|
|
122
|
-
|
|
123
|
-
normalizeBundlePath(file.relativePath);
|
|
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 };
|
|
124
151
|
}
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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);
|
|
129
163
|
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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
|
+
}
|
|
139
180
|
}
|
|
140
181
|
}
|
|
141
182
|
let entry;
|
|
@@ -149,15 +190,24 @@ async function startSession(app, deps, body, reply) {
|
|
|
149
190
|
}
|
|
150
191
|
throw err;
|
|
151
192
|
}
|
|
193
|
+
// For an app session, drop client-supplied values for any env key the
|
|
194
|
+
// catalog defines (a client must never override operator-held values, which
|
|
195
|
+
// include secrets), then inject the operator's values.
|
|
196
|
+
const clientEnv = appEntry
|
|
197
|
+
? {
|
|
198
|
+
...Object.fromEntries(Object.entries(body.env).filter(([key]) => !(key in appEntry.env))),
|
|
199
|
+
...appEntry.env,
|
|
200
|
+
}
|
|
201
|
+
: body.env;
|
|
152
202
|
// Surface a TELO_REGISTRY_URL to the workload so the telo CLI inside picks
|
|
153
|
-
// it up. Precedence:
|
|
154
|
-
//
|
|
155
|
-
//
|
|
156
|
-
const configRegistryUrl =
|
|
203
|
+
// it up. Precedence: explicit env value > config.registryUrl (per-request
|
|
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;
|
|
157
207
|
const registryUrl = configRegistryUrl ?? deps.defaultRegistryUrl;
|
|
158
|
-
const sessionEnv = registryUrl && !("TELO_REGISTRY_URL" in
|
|
159
|
-
? { ...
|
|
160
|
-
:
|
|
208
|
+
const sessionEnv = registryUrl && !("TELO_REGISTRY_URL" in clientEnv)
|
|
209
|
+
? { ...clientEnv, TELO_REGISTRY_URL: registryUrl }
|
|
210
|
+
: clientEnv;
|
|
161
211
|
// Respond as soon as the session is registered — BEFORE the backend starts.
|
|
162
212
|
// `backend.start()` now spans the on-cluster image build and pod bring-up,
|
|
163
213
|
// which can take seconds-to-minutes; awaiting it here would hide the event
|
|
@@ -173,11 +223,12 @@ async function startSession(app, deps, body, reply) {
|
|
|
173
223
|
deps.backend
|
|
174
224
|
.start({
|
|
175
225
|
sessionId,
|
|
176
|
-
bundle
|
|
226
|
+
bundle,
|
|
177
227
|
entryRelativePath: entryRelative,
|
|
178
228
|
env: sessionEnv,
|
|
179
229
|
ports: body.ports ?? [],
|
|
180
|
-
config
|
|
230
|
+
config,
|
|
231
|
+
selfContained: appEntry !== undefined,
|
|
181
232
|
inspect: body.inspect ?? false,
|
|
182
233
|
onStatus: (status) => deps.registry.emit(sessionId, { type: "status", status }),
|
|
183
234
|
onProgress: (phase, message, done) => deps.registry.emit(sessionId, { type: "progress", phase, message, done }),
|
|
@@ -190,6 +241,7 @@ async function startSession(app, deps, body, reply) {
|
|
|
190
241
|
if (isEventFrame(frame))
|
|
191
242
|
deps.registry.emit(sessionId, { type: "debug", frame });
|
|
192
243
|
},
|
|
244
|
+
onReachability: (port, state) => deps.registry.emit(sessionId, { type: "reachability", port, state }),
|
|
193
245
|
isUserStopped: () => entry.userStopped,
|
|
194
246
|
})
|
|
195
247
|
.then(async (session) => {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sessions.js","sourceRoot":"","sources":["../../src/routes/sessions.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAEnD,OAAO,EACL,qBAAqB,EACrB,iBAAiB,
|
|
1
|
+
{"version":3,"file":"sessions.js","sourceRoot":"","sources":["../../src/routes/sessions.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAEnD,OAAO,EACL,qBAAqB,EACrB,iBAAiB,GAKlB,MAAM,gBAAgB,CAAC;AAExB,OAAO,EAAE,eAAe,EAAE,mBAAmB,EAAE,MAAM,2BAA2B,CAAC;AACjF,OAAO,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAC7D,OAAO,EAAE,iBAAiB,EAAwB,MAAM,wBAAwB,CAAC;AACjF,OAAO,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AAuBxD,gFAAgF;AAChF,iEAAiE;AACjE,MAAM,eAAe,GAAG;IACtB,IAAI,EAAE,QAAQ;IACd,QAAQ,EAAE,CAAC,KAAK,CAAC;IACjB,UAAU,EAAE;QACV,GAAG,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,SAAS,EAAE,CAAC,EAAE;QACrC,MAAM,EAAE;YACN,IAAI,EAAE,QAAQ;YACd,QAAQ,EAAE,CAAC,mBAAmB,EAAE,OAAO,CAAC;YACxC,UAAU,EAAE;gBACV,iBAAiB,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,SAAS,EAAE,CAAC,EAAE;gBACnD,KAAK,EAAE;oBACL,IAAI,EAAE,OAAO;oBACb,KAAK,EAAE;wBACL,IAAI,EAAE,QAAQ;wBACd,QAAQ,EAAE,CAAC,cAAc,EAAE,UAAU,CAAC;wBACtC,UAAU,EAAE;4BACV,YAAY,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,SAAS,EAAE,CAAC,EAAE;4BAC9C,QAAQ,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE;yBAC7B;qBACF;iBACF;aACF;SACF;QACD,GAAG,EAAE;YACH,IAAI,EAAE,QAAQ;YACd,oBAAoB,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE;SACzC;QACD,KAAK,EAAE;YACL,IAAI,EAAE,OAAO;YACb,KAAK,EAAE;gBACL,IAAI,EAAE,QAAQ;gBACd,QAAQ,EAAE,CAAC,MAAM,EAAE,UAAU,CAAC;gBAC9B,UAAU,EAAE;oBACV,IAAI,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,CAAC,EAAE,OAAO,EAAE,KAAK,EAAE;oBACrD,QAAQ,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,KAAK,EAAE,KAAK,CAAC,EAAE;iBACnD;aACF;SACF;QACD,MAAM,EAAE;YACN,IAAI,EAAE,QAAQ;YACd,QAAQ,EAAE,CAAC,OAAO,EAAE,YAAY,CAAC;YACjC,UAAU,EAAE;gBACV,KAAK,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,SAAS,EAAE,CAAC,EAAE;gBACvC,UAAU,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,SAAS,EAAE,QAAQ,EAAE,OAAO,CAAC,EAAE;gBACpE,WAAW,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,SAAS,EAAE,CAAC,EAAE;aAC9C;SACF;QACD,OAAO,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE;KAC7B;CACO,CAAC;AAEX,MAAM,UAAU,aAAa,CAAC,IAAuB;IACnD,OAAO,KAAK,EAAE,GAAoB,EAAE,EAAE;QACpC,GAAG,CAAC,IAAI,CACN,cAAc,EACd,EAAE,MAAM,EAAE,EAAE,IAAI,EAAE,eAAe,EAAE,EAAE,EACrC,KAAK,EAAE,GAAG,EAAE,KAAK,EAAE,EAAE;YACnB,0EAA0E;YAC1E,0EAA0E;YAC1E,6BAA6B;YAC7B,IAAI,IAAI,CAAC,KAAK,EAAE,CAAC;gBACf,MAAM,GAAG,GAAG,GAAG,CAAC,OAAO,CAAC,qBAAqB,CAAC,CAAC;gBAC/C,MAAM,QAAQ,GAAG,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC;gBACnD,IAAI,QAAQ,KAAK,IAAI,CAAC,KAAK,CAAC,OAAO,EAAE,CAAC;oBACpC,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,gBAAgB,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,CAAC,CAAC;oBACrE,OAAO;gBACT,CAAC;YACH,CAAC;YACD,OAAO,YAAY,CAAC,GAAG,EAAE,IAAI,EAAE,GAAG,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;QAClD,CAAC,CACF,CAAC;QAEF,GAAG,CAAC,GAAG,CAA6B,kBAAkB,EAAE,KAAK,EAAE,GAAG,EAAE,KAAK,EAAE,EAAE;YAC3E,MAAM,KAAK,GAAG,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;YAC/C,IAAI,CAAC,KAAK,EAAE,CAAC;gBACX,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,WAAW,EAAE,OAAO,EAAE,YAAY,GAAG,CAAC,MAAM,CAAC,EAAE,mBAAmB,EAAE,CAAC,CAAC;gBACpG,OAAO;YACT,CAAC;YACD,KAAK,CAAC,IAAI,CAAC;gBACT,SAAS,EAAE,KAAK,CAAC,SAAS;gBAC1B,MAAM,EAAE,KAAK,CAAC,MAAM;gBACpB,SAAS,EAAE,KAAK,CAAC,SAAS,CAAC,WAAW,EAAE;gBACxC,QAAQ,EAAE,KAAK,CAAC,QAAQ,EAAE,WAAW,EAAE;aACxC,CAAC,CAAC;QACL,CAAC,CAAC,CAAC;QAEH,GAAG,CAAC,MAAM,CAA6B,kBAAkB,EAAE,KAAK,EAAE,GAAG,EAAE,KAAK,EAAE,EAAE;YAC9E,MAAM,KAAK,GAAG,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;YAC/C,IAAI,CAAC,KAAK,EAAE,CAAC;gBACX,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC;gBACvB,OAAO;YACT,CAAC;YACD,KAAK,CAAC,WAAW,GAAG,IAAI,CAAC;YACzB,IAAI,KAAK,CAAC,OAAO,EAAE,CAAC;gBAClB,IAAI,CAAC;oBACH,MAAM,KAAK,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC;gBAC7B,CAAC;gBAAC,OAAO,GAAG,EAAE,CAAC;oBACb,GAAG,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,GAAG,EAAE,SAAS,EAAE,KAAK,CAAC,SAAS,EAAE,EAAE,wBAAwB,CAAC,CAAC;oBAC7E,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,aAAa,EAAE,OAAO,EAAG,GAAa,CAAC,OAAO,EAAE,CAAC,CAAC;oBAChF,OAAO;gBACT,CAAC;YACH,CAAC;YACD,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC;QACzB,CAAC,CAAC,CAAC;QAEH,GAAG,CAAC,GAAG,CACL,yBAAyB,EACzB,KAAK,EAAE,GAAG,EAAE,KAAK,EAAE,EAAE,CACnB,mBAAmB,CAAC;YAClB,QAAQ,EAAE,IAAI,CAAC,QAAQ;YACvB,GAAG;YACH,KAAK;YACL,SAAS,EAAE,GAAG,CAAC,MAAM,CAAC,EAAE;YACxB,WAAW,EAAE,IAAI,CAAC,WAAW;SAC9B,CAAC,CACL,CAAC;IACJ,CAAC,CAAC;AACJ,CAAC;AAED,KAAK,UAAU,YAAY,CACzB,GAAoB,EACpB,IAAuB,EACvB,IAAyB,EACzB,KAAmB;IAEnB,MAAM,SAAS,GAAG,iBAAiB,EAAE,CAAC;IAEtC,wEAAwE;IACxE,6EAA6E;IAC7E,yEAAyE;IACzE,iCAAiC;IACjC,MAAM,QAAQ,GAAG,IAAI,CAAC,GAAG,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IAC5E,IAAI,IAAI,CAAC,GAAG,KAAK,SAAS,IAAI,CAAC,QAAQ,EAAE,CAAC;QACxC,MAAM,OAAO,GAAG,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC;QAC7C,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC;YACnB,KAAK,EAAE,aAAa;YACpB,OAAO,EACL,QAAQ,IAAI,CAAC,GAAG,iCAAiC;gBACjD,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC;oBACjB,CAAC,CAAC,oBAAoB,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE;oBAC1C,CAAC,CAAC,yCAAyC,CAAC;gBAC9C,0BAA0B;SAC7B,CAAC,CAAC;QACH,OAAO;IACT,CAAC;IACD,IAAI,CAAC,QAAQ,IAAI,CAAC,CAAC,IAAI,CAAC,MAAM,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC;QAChD,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC;YACnB,KAAK,EAAE,iBAAiB;YACxB,OAAO,EAAE,iFAAiF;SAC3F,CAAC,CAAC;QACH,OAAO;IACT,CAAC;IAED,IAAI,MAAiB,CAAC;IACtB,IAAI,aAAqB,CAAC;IAC1B,IAAI,MAAqB,CAAC;IAC1B,IAAI,QAAQ,EAAE,CAAC;QACb,2EAA2E;QAC3E,+CAA+C;QAC/C,MAAM,GAAG,EAAE,iBAAiB,EAAE,WAAW,EAAE,KAAK,EAAE,EAAE,EAAE,CAAC;QACvD,aAAa,GAAG,MAAM,CAAC,iBAAiB,CAAC;QACzC,MAAM,GAAG,EAAE,KAAK,EAAE,QAAQ,CAAC,KAAK,EAAE,UAAU,EAAE,QAAQ,CAAC,UAAU,EAAE,CAAC;IACtE,CAAC;SAAM,CAAC;QACN,MAAM,GAAG,IAAI,CAAC,MAAO,CAAC;QACtB,MAAM,GAAG,IAAI,CAAC,MAAO,CAAC;QACtB,IAAI,CAAC;YACH,wEAAwE;YACxE,wEAAwE;YACxE,yEAAyE;YACzE,qEAAqE;YACrE,aAAa,GAAG,mBAAmB,CAAC,MAAM,CAAC,iBAAiB,CAAC,CAAC;YAC9D,KAAK,MAAM,IAAI,IAAI,MAAM,CAAC,KAAK;gBAAE,mBAAmB,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC;QAC1E,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,IAAI,GAAG,YAAY,eAAe,EAAE,CAAC;gBACnC,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,gBAAgB,EAAE,OAAO,EAAE,GAAG,CAAC,OAAO,EAAE,CAAC,CAAC;gBACxE,OAAO;YACT,CAAC;YACD,MAAM,GAAG,CAAC;QACZ,CAAC;QAED,6EAA6E;QAC7E,uEAAuE;QACvE,yEAAyE;QACzE,IAAI,IAAI,CAAC,cAAc,EAAE,CAAC;YACxB,MAAM,OAAO,GAAG,IAAI,CAAC,cAAc,CAAC,MAAM,CAAC,CAAC;YAC5C,IAAI,OAAO,EAAE,CAAC;gBACZ,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,gBAAgB,EAAE,OAAO,EAAE,CAAC,CAAC;gBAC3D,OAAO;YACT,CAAC;QACH,CAAC;IACH,CAAC;IAED,IAAI,KAA8C,CAAC;IACnD,IAAI,CAAC;QACH,KAAK,GAAG,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE,SAAS,EAAE,CAAC,CAAC;IAChD,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,IAAI,GAAG,YAAY,iBAAiB,EAAE,CAAC;YACrC,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,mBAAmB,EAAE,OAAO,EAAE,GAAG,CAAC,OAAO,EAAE,CAAC,CAAC;YAC3E,OAAO;QACT,CAAC;QACD,MAAM,GAAG,CAAC;IACZ,CAAC;IAED,sEAAsE;IACtE,4EAA4E;IAC5E,uDAAuD;IACvD,MAAM,SAAS,GAAG,QAAQ;QACxB,CAAC,CAAC;YACE,GAAG,MAAM,CAAC,WAAW,CACnB,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,GAAG,IAAI,QAAQ,CAAC,GAAG,CAAC,CAAC,CACnE;YACD,GAAG,QAAQ,CAAC,GAAG;SAChB;QACH,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC;IAEb,2EAA2E;IAC3E,0EAA0E;IAC1E,uEAAuE;IACvE,kEAAkE;IAClE,MAAM,iBAAiB,GAAG,MAAM,CAAC,WAAW,EAAE,IAAI,EAAE,IAAI,SAAS,CAAC;IAClE,MAAM,WAAW,GAAG,iBAAiB,IAAI,IAAI,CAAC,kBAAkB,CAAC;IACjE,MAAM,UAAU,GACd,WAAW,IAAI,CAAC,CAAC,mBAAmB,IAAI,SAAS,CAAC;QAChD,CAAC,CAAC,EAAE,GAAG,SAAS,EAAE,iBAAiB,EAAE,WAAW,EAAE;QAClD,CAAC,CAAC,SAAS,CAAC;IAEhB,4EAA4E;IAC5E,2EAA2E;IAC3E,2EAA2E;IAC3E,4EAA4E;IAC5E,yEAAyE;IACzE,6EAA6E;IAC7E,oDAAoD;IACpD,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC;QACnB,SAAS;QACT,SAAS,EAAE,gBAAgB,SAAS,SAAS;QAC7C,SAAS,EAAE,KAAK,CAAC,SAAS,CAAC,WAAW,EAAE;KACzC,CAAC,CAAC;IAEH,IAAI,CAAC,OAAO;SACT,KAAK,CAAC;QACL,SAAS;QACT,MAAM;QACN,iBAAiB,EAAE,aAAa;QAChC,GAAG,EAAE,UAAU;QACf,KAAK,EAAE,IAAI,CAAC,KAAK,IAAI,EAAE;QACvB,MAAM;QACN,aAAa,EAAE,QAAQ,KAAK,SAAS;QACrC,OAAO,EAAE,IAAI,CAAC,OAAO,IAAI,KAAK;QAC9B,QAAQ,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,SAAS,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,CAAC;QAC/E,UAAU,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,EAAE,CACnC,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,SAAS,EAAE,EAAE,IAAI,EAAE,UAAU,EAAE,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;QAC3E,QAAQ,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,SAAS,CAAC,SAAS,EAAE,KAAK,CAAC;QAC9D,wEAAwE;QACxE,0EAA0E;QAC1E,sEAAsE;QACtE,sEAAsE;QACtE,OAAO,EAAE,CAAC,KAAK,EAAE,EAAE;YACjB,IAAI,YAAY,CAAC,KAAK,CAAC;gBAAE,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,SAAS,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC,CAAC;QACnF,CAAC;QACD,cAAc,EAAE,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAC9B,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,SAAS,EAAE,EAAE,IAAI,EAAE,cAAc,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC;QACtE,aAAa,EAAE,GAAG,EAAE,CAAC,KAAK,CAAC,WAAW;KACvC,CAAC;SACD,IAAI,CAAC,KAAK,EAAE,OAAO,EAAE,EAAE;QACtB,KAAK,CAAC,OAAO,GAAG,OAAO,CAAC;QACxB,sEAAsE;QACtE,sEAAsE;QACtE,yEAAyE;QACzE,qCAAqC;QACrC,IAAI,KAAK,CAAC,WAAW,EAAE,CAAC;YACtB,IAAI,CAAC;gBACH,MAAM,OAAO,CAAC,IAAI,EAAE,CAAC;YACvB,CAAC;YAAC,OAAO,GAAG,EAAE,CAAC;gBACb,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,GAAG,EAAE,SAAS,EAAE,EAAE,iDAAiD,CAAC,CAAC;YACtF,CAAC;QACH,CAAC;IACH,CAAC,CAAC;SACD,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE;QACb,qEAAqE;QACrE,sEAAsE;QACtE,8DAA8D;QAC9D,MAAM,OAAO,GACX,GAAG,YAAY,iBAAiB;YAC9B,CAAC,CAAC,GAAG,GAAG,CAAC,KAAK,KAAK,GAAG,CAAC,OAAO,EAAE;YAChC,CAAC,CAAC,GAAG,YAAY,KAAK;gBACpB,CAAC,CAAC,GAAG,CAAC,OAAO;gBACb,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QACpB,GAAG,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,GAAG,EAAE,SAAS,EAAE,EAAE,sBAAsB,CAAC,CAAC;QAC1D,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,SAAS,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,EAAE,CAAC,CAAC;IACzF,CAAC,CAAC,CAAC;AACP,CAAC"}
|
package/dist/server.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type FastifyBaseLogger, type FastifyInstance } from "fastify";
|
|
2
2
|
import type { RunnerBackend } from "./backend.js";
|
|
3
|
-
import type { RunnerCoreConfig } from "./config.js";
|
|
3
|
+
import type { ResolvedRunnerApp, RunnerCoreConfig } from "./config.js";
|
|
4
4
|
import type { RunnerCapabilities, SessionConfig } from "./contract.js";
|
|
5
5
|
import { SessionRegistry } from "./session/registry.js";
|
|
6
6
|
export interface ServerDeps {
|
|
@@ -16,8 +16,13 @@ export interface ServerDeps {
|
|
|
16
16
|
/** Runner's default registry URL, passed to workloads as TELO_REGISTRY_URL. */
|
|
17
17
|
defaultRegistryUrl?: string;
|
|
18
18
|
/** Backend config gate, enforced on `POST /v1/sessions` before the workload
|
|
19
|
-
* starts (e.g. an `image` allowlist). Rejects with `400 invalid_config`.
|
|
19
|
+
* starts (e.g. an `image` allowlist). Rejects with `400 invalid_config`.
|
|
20
|
+
* Not consulted for app sessions — their image comes from `apps`. */
|
|
20
21
|
validateConfig?: (config: SessionConfig) => string | undefined;
|
|
22
|
+
/** Operator-predefined applications launchable by name (usually
|
|
23
|
+
* `loadResolvedApps(process.env)`). Advertised on /v1/capabilities as
|
|
24
|
+
* `apps` descriptors; the session route resolves and gates against it. */
|
|
25
|
+
apps?: Record<string, ResolvedRunnerApp>;
|
|
21
26
|
registry?: SessionRegistry;
|
|
22
27
|
}
|
|
23
28
|
export interface ServerHandle {
|
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,gBAAgB,EAAE,MAAM,aAAa,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;AAM5F,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;;+EAE2E;IAC3E,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,CA4DzE;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
|
@@ -25,11 +25,19 @@ export async function buildServer(deps) {
|
|
|
25
25
|
exitTtlMs: deps.config.exitTtlMs,
|
|
26
26
|
replayBufferBytes: deps.config.replayBufferBytes,
|
|
27
27
|
});
|
|
28
|
+
// The app catalog is injected into the served capabilities document here, so
|
|
29
|
+
// what /v1/capabilities advertises and what the session route accepts can
|
|
30
|
+
// never drift — both come from `deps.apps`.
|
|
31
|
+
const appDescriptors = Object.values(deps.apps ?? {}).map(({ name, title, description }) => ({ name, title, description }));
|
|
32
|
+
const withApps = (caps) => appDescriptors.length > 0 ? { ...caps, apps: appDescriptors } : caps;
|
|
33
|
+
const capabilitiesGetter = typeof deps.capabilities === "function"
|
|
34
|
+
? () => withApps(deps.capabilities())
|
|
35
|
+
: withApps(deps.capabilities);
|
|
28
36
|
// Terms are stable across the process — resolve the capabilities once for them
|
|
29
37
|
// even when `capabilities` is a getter (the route still re-resolves per request).
|
|
30
|
-
const capabilitiesValue = typeof
|
|
38
|
+
const capabilitiesValue = typeof capabilitiesGetter === "function" ? capabilitiesGetter() : capabilitiesGetter;
|
|
31
39
|
await app.register(healthRoute(deps.version));
|
|
32
|
-
await app.register(capabilitiesRoute(
|
|
40
|
+
await app.register(capabilitiesRoute(capabilitiesGetter));
|
|
33
41
|
await app.register(probeRoute({ backend: deps.backend }));
|
|
34
42
|
await app.register(sessionsRoute({
|
|
35
43
|
backend: deps.backend,
|
|
@@ -40,6 +48,7 @@ export async function buildServer(deps) {
|
|
|
40
48
|
// The capabilities document is the single source of the runner's terms;
|
|
41
49
|
// the session route enforces what /v1/capabilities advertises.
|
|
42
50
|
terms: capabilitiesValue.terms,
|
|
51
|
+
apps: deps.apps,
|
|
43
52
|
}));
|
|
44
53
|
await app.register(ioRoute({ registry, corsOrigins: deps.config.corsOrigins }));
|
|
45
54
|
return { app, registry };
|
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":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@telorun/runner-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
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",
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve after `ms`, or immediately when `signal` aborts. Safe to call in a
|
|
3
|
+
* poll loop on a shared signal: it removes its abort listener on the timer path
|
|
4
|
+
* (so repeated calls can't accumulate listeners) and fast-paths an
|
|
5
|
+
* already-aborted signal (so a loop tears down promptly instead of waiting out
|
|
6
|
+
* one more interval). The timer is `unref`'d so a pending delay can't keep the
|
|
7
|
+
* process alive on its own.
|
|
8
|
+
*/
|
|
9
|
+
export function abortableDelay(ms: number, signal: AbortSignal): Promise<void> {
|
|
10
|
+
return new Promise((resolve) => {
|
|
11
|
+
if (signal.aborted) return resolve();
|
|
12
|
+
let timer: ReturnType<typeof setTimeout>;
|
|
13
|
+
const onAbort = (): void => {
|
|
14
|
+
clearTimeout(timer);
|
|
15
|
+
resolve();
|
|
16
|
+
};
|
|
17
|
+
timer = setTimeout(() => {
|
|
18
|
+
signal.removeEventListener("abort", onAbort);
|
|
19
|
+
resolve();
|
|
20
|
+
}, ms);
|
|
21
|
+
timer.unref?.();
|
|
22
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
23
|
+
});
|
|
24
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { loadAppsFromEnv, loadResolvedApps, RunnerConfigError } from "./config.js";
|
|
4
|
+
|
|
5
|
+
describe("loadAppsFromEnv", () => {
|
|
6
|
+
it("returns undefined when RUNNER_APPS is unset (no apps offered)", () => {
|
|
7
|
+
expect(loadAppsFromEnv({})).toBeUndefined();
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it("parses a valid catalog", () => {
|
|
11
|
+
const catalog = loadAppsFromEnv({
|
|
12
|
+
RUNNER_APPS: JSON.stringify({
|
|
13
|
+
tool: {
|
|
14
|
+
image: "acme/tool:1.2.0",
|
|
15
|
+
env: { SERVICE_TOKEN: "tok-op" },
|
|
16
|
+
pullPolicy: "never",
|
|
17
|
+
},
|
|
18
|
+
}),
|
|
19
|
+
})!;
|
|
20
|
+
expect(catalog.tool.image).toBe("acme/tool:1.2.0");
|
|
21
|
+
expect(catalog.tool.env).toEqual({ SERVICE_TOKEN: "tok-op" });
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("rejects malformed JSON and invalid entries loudly", () => {
|
|
25
|
+
expect(() => loadAppsFromEnv({ RUNNER_APPS: "{nope" })).toThrow(RunnerConfigError);
|
|
26
|
+
expect(() => loadAppsFromEnv({ RUNNER_APPS: '{"x":{}}' })).toThrow(/needs a non-empty string 'image'/);
|
|
27
|
+
expect(() => loadAppsFromEnv({ RUNNER_APPS: '{"x":{"image":"i","env":["nope"]}}' })).toThrow(
|
|
28
|
+
/invalid 'env'/,
|
|
29
|
+
);
|
|
30
|
+
expect(() =>
|
|
31
|
+
loadAppsFromEnv({ RUNNER_APPS: '{"x":{"image":"i","pullPolicy":"sometimes"}}' }),
|
|
32
|
+
).toThrow(/pullPolicy/);
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
describe("loadResolvedApps", () => {
|
|
37
|
+
it("is empty when RUNNER_APPS is unset", () => {
|
|
38
|
+
expect(loadResolvedApps({})).toEqual({});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("applies defaults to the configured catalog", () => {
|
|
42
|
+
const resolved = loadResolvedApps({
|
|
43
|
+
RUNNER_APPS: '{"tool":{"image":"acme/tool:1"}}',
|
|
44
|
+
});
|
|
45
|
+
expect(resolved.tool).toEqual({
|
|
46
|
+
name: "tool",
|
|
47
|
+
image: "acme/tool:1",
|
|
48
|
+
env: {},
|
|
49
|
+
pullPolicy: "missing",
|
|
50
|
+
title: undefined,
|
|
51
|
+
description: undefined,
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
});
|
package/src/backend.ts
CHANGED
|
@@ -4,6 +4,7 @@ import type {
|
|
|
4
4
|
AvailabilityReport,
|
|
5
5
|
PortMapping,
|
|
6
6
|
ProbeConfig,
|
|
7
|
+
ReachabilityState,
|
|
7
8
|
RunBundle,
|
|
8
9
|
RunPhase,
|
|
9
10
|
RunStatus,
|
|
@@ -47,6 +48,11 @@ export interface BackendStartSpec {
|
|
|
47
48
|
env: Record<string, string>;
|
|
48
49
|
ports: PortMapping[];
|
|
49
50
|
config: SessionConfig;
|
|
51
|
+
/** True for an operator-predefined app session (`StartSessionRequest.app`):
|
|
52
|
+
* `config.image` is self-contained (app + controllers baked in), so the
|
|
53
|
+
* backend runs the image's own entrypoint and stages no bundle — `bundle`
|
|
54
|
+
* is an empty placeholder. */
|
|
55
|
+
selfContained: boolean;
|
|
50
56
|
/** When true, launch the workload with `--inspect` and relay its kernel debug
|
|
51
57
|
* stream via `onDebug`. The inspect endpoint stays reachable only by the
|
|
52
58
|
* runner — never published outward. */
|
|
@@ -64,6 +70,10 @@ export interface BackendStartSpec {
|
|
|
64
70
|
/** A frame relayed from the workload's kernel debug stream. Only called when
|
|
65
71
|
* `inspect` is true and the backend has connected to the inspect endpoint. */
|
|
66
72
|
onDebug(frame: DebugFrame): void;
|
|
73
|
+
/** Report a declared port's reachability from the runner network — `checking`
|
|
74
|
+
* while the workload comes up, then `reachable`, or `unreachable` after a
|
|
75
|
+
* timeout. Surfaced on the editor's endpoint badge, not the log stream. */
|
|
76
|
+
onReachability(port: number, state: ReachabilityState): void;
|
|
67
77
|
/** True once a user stop / shutdown has been requested — lets the backend
|
|
68
78
|
* classify a kill as `stopped` rather than `failed`. */
|
|
69
79
|
isUserStopped(): boolean;
|
package/src/config.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { readFileSync } from "node:fs";
|
|
3
3
|
|
|
4
|
-
import type { RunnerTerms } from "./contract.js";
|
|
4
|
+
import type { PullPolicy, RunnerTerms } from "./contract.js";
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Backend-neutral runner configuration. Concrete runners (docker, k8s) extend
|
|
@@ -91,6 +91,121 @@ function hashTermsVersion(body: string): string {
|
|
|
91
91
|
return createHash("sha256").update(body).digest("hex").slice(0, 12);
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
+
/**
|
|
95
|
+
* An operator-predefined application entry in `RUNNER_APPS`. The catalog is the
|
|
96
|
+
* whole gate: a client launches an app by name and can neither pick the image
|
|
97
|
+
* nor read the injected env, so no allowlist beyond the catalog is needed.
|
|
98
|
+
*
|
|
99
|
+
* The catalog may embed secrets (`env` values), so treat the whole `RUNNER_APPS`
|
|
100
|
+
* value as secret material (docker: a `.env.local` file; k8s: source it from a
|
|
101
|
+
* Secret). Only `name`/`title`/`description` are ever advertised outward —
|
|
102
|
+
* `image` and `env` never leave the runner.
|
|
103
|
+
*/
|
|
104
|
+
export interface RunnerAppConfig {
|
|
105
|
+
image: string;
|
|
106
|
+
/** Env injected verbatim into the app's workload (operator secrets included).
|
|
107
|
+
* A client-supplied value for any key defined here is dropped — operator
|
|
108
|
+
* values always win. */
|
|
109
|
+
env?: Record<string, string>;
|
|
110
|
+
/** Workload image pull policy (default `missing`); `always` keeps a moving
|
|
111
|
+
* tag like `latest-slim` fresh. */
|
|
112
|
+
pullPolicy?: PullPolicy;
|
|
113
|
+
title?: string;
|
|
114
|
+
description?: string;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** A validated catalog entry with defaults applied, keyed back by its name. */
|
|
118
|
+
export interface ResolvedRunnerApp {
|
|
119
|
+
name: string;
|
|
120
|
+
image: string;
|
|
121
|
+
env: Record<string, string>;
|
|
122
|
+
pullPolicy: PullPolicy;
|
|
123
|
+
title?: string;
|
|
124
|
+
description?: string;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const PULL_POLICIES: readonly string[] = ["missing", "always", "never"];
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Parse the `RUNNER_APPS` JSON catalog ({ "<name>": RunnerAppConfig }).
|
|
131
|
+
* Returns `undefined` when unset (no apps offered). Malformed JSON or entries
|
|
132
|
+
* are a hard `RunnerConfigError` — a broken catalog must not silently drop
|
|
133
|
+
* the apps.
|
|
134
|
+
*/
|
|
135
|
+
export function loadAppsFromEnv(
|
|
136
|
+
env: NodeJS.ProcessEnv,
|
|
137
|
+
): Record<string, RunnerAppConfig> | undefined {
|
|
138
|
+
const raw = env.RUNNER_APPS?.trim();
|
|
139
|
+
if (!raw) return undefined;
|
|
140
|
+
let parsed: unknown;
|
|
141
|
+
try {
|
|
142
|
+
parsed = JSON.parse(raw);
|
|
143
|
+
} catch (err) {
|
|
144
|
+
throw new RunnerConfigError(`RUNNER_APPS is not valid JSON: ${(err as Error).message}`);
|
|
145
|
+
}
|
|
146
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
147
|
+
throw new RunnerConfigError("RUNNER_APPS must be a JSON object mapping app name → app config.");
|
|
148
|
+
}
|
|
149
|
+
// Null-prototype map so an app literally named `__proto__` is stored as a
|
|
150
|
+
// plain key rather than mutating the object's prototype.
|
|
151
|
+
const catalog: Record<string, RunnerAppConfig> = Object.create(null);
|
|
152
|
+
for (const [name, value] of Object.entries(parsed)) {
|
|
153
|
+
catalog[name] = validateAppEntry(name, value);
|
|
154
|
+
}
|
|
155
|
+
return catalog;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function validateAppEntry(name: string, value: unknown): RunnerAppConfig {
|
|
159
|
+
const fail = (detail: string): never => {
|
|
160
|
+
throw new RunnerConfigError(`RUNNER_APPS entry '${name}' ${detail}`);
|
|
161
|
+
};
|
|
162
|
+
if (name.trim() === "") fail("has an empty name.");
|
|
163
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
164
|
+
fail("must be an object.");
|
|
165
|
+
}
|
|
166
|
+
const entry = value as Record<string, unknown>;
|
|
167
|
+
if (typeof entry.image !== "string" || entry.image.trim() === "") {
|
|
168
|
+
fail("needs a non-empty string 'image'.");
|
|
169
|
+
}
|
|
170
|
+
if (
|
|
171
|
+
entry.env !== undefined &&
|
|
172
|
+
(entry.env === null ||
|
|
173
|
+
typeof entry.env !== "object" ||
|
|
174
|
+
Array.isArray(entry.env) ||
|
|
175
|
+
Object.values(entry.env).some((v) => typeof v !== "string"))
|
|
176
|
+
) {
|
|
177
|
+
fail("has an invalid 'env' — expected an object of string values.");
|
|
178
|
+
}
|
|
179
|
+
if (entry.pullPolicy !== undefined && !PULL_POLICIES.includes(entry.pullPolicy as string)) {
|
|
180
|
+
fail(`has an invalid 'pullPolicy' — expected one of ${PULL_POLICIES.join(", ")}.`);
|
|
181
|
+
}
|
|
182
|
+
for (const key of ["title", "description"] as const) {
|
|
183
|
+
if (entry[key] !== undefined && typeof entry[key] !== "string") {
|
|
184
|
+
fail(`has an invalid '${key}' — expected a string.`);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return entry as unknown as RunnerAppConfig;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** The catalog runners pass to `buildServer`: `RUNNER_APPS` validated with
|
|
191
|
+
* defaults applied; empty when unset. The catalog is pure operator
|
|
192
|
+
* configuration — runner-core knows nothing about any specific app. */
|
|
193
|
+
export function loadResolvedApps(env: NodeJS.ProcessEnv): Record<string, ResolvedRunnerApp> {
|
|
194
|
+
const catalog = loadAppsFromEnv(env) ?? {};
|
|
195
|
+
const resolved: Record<string, ResolvedRunnerApp> = Object.create(null);
|
|
196
|
+
for (const [name, entry] of Object.entries(catalog)) {
|
|
197
|
+
resolved[name] = {
|
|
198
|
+
name,
|
|
199
|
+
image: entry.image,
|
|
200
|
+
env: entry.env ?? {},
|
|
201
|
+
pullPolicy: entry.pullPolicy ?? "missing",
|
|
202
|
+
title: entry.title,
|
|
203
|
+
description: entry.description,
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
return resolved;
|
|
207
|
+
}
|
|
208
|
+
|
|
94
209
|
export function parseCorsOrigins(raw: string | undefined): string[] | "*" {
|
|
95
210
|
if (raw === undefined || raw.trim() === "" || raw.trim() === "*") return "*";
|
|
96
211
|
return raw
|
package/src/contract.ts
CHANGED
|
@@ -29,6 +29,10 @@ export interface RunnerCapabilities {
|
|
|
29
29
|
* with `428 terms_required` unless the client sends `x-telo-accepted-terms`
|
|
30
30
|
* matching `terms.version`. The editor surfaces it and records acceptance. */
|
|
31
31
|
terms?: RunnerTerms;
|
|
32
|
+
/** Operator-predefined applications this runner can launch by name
|
|
33
|
+
* (`StartSessionRequest.app`). Absent/empty when none are offered — clients
|
|
34
|
+
* hide the corresponding entry points. */
|
|
35
|
+
apps?: RunnerAppDescriptor[];
|
|
32
36
|
}
|
|
33
37
|
|
|
34
38
|
/** An operator-defined agreement. `version` is opaque and operator-controlled;
|
|
@@ -49,6 +53,18 @@ export interface RunnerFeatures {
|
|
|
49
53
|
ports: boolean;
|
|
50
54
|
}
|
|
51
55
|
|
|
56
|
+
/** An operator-predefined application the runner can launch by name. Only the
|
|
57
|
+
* identity is advertised — the image and the operator env injected into it
|
|
58
|
+
* stay server-side, so a client can never pick the image or reach the
|
|
59
|
+
* secrets. Which apps exist is pure operator configuration (`RUNNER_APPS`);
|
|
60
|
+
* the runner has no built-in knowledge of any specific app. */
|
|
61
|
+
export interface RunnerAppDescriptor {
|
|
62
|
+
/** Wire id requested via `StartSessionRequest.app`. */
|
|
63
|
+
name: string;
|
|
64
|
+
title?: string;
|
|
65
|
+
description?: string;
|
|
66
|
+
}
|
|
67
|
+
|
|
52
68
|
/** A JSON Schema document, kept structurally open so runner-core need not depend
|
|
53
69
|
* on a JSON-Schema type package. The editor treats it as `JSONSchema7`. */
|
|
54
70
|
export type JsonSchema = Record<string, unknown>;
|
|
@@ -103,10 +119,17 @@ export interface RunnerEndpoint {
|
|
|
103
119
|
}
|
|
104
120
|
|
|
105
121
|
export interface StartSessionRequest {
|
|
106
|
-
|
|
122
|
+
/** Launch an operator-predefined application by name (see
|
|
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;
|
|
107
129
|
env: Record<string, string>;
|
|
108
130
|
ports?: PortMapping[];
|
|
109
|
-
|
|
131
|
+
/** Required unless `app` is set. */
|
|
132
|
+
config?: SessionConfig;
|
|
110
133
|
/** Request the kernel debug stream. When true the runner launches the
|
|
111
134
|
* workload with `--inspect`, subscribes to the in-workload inspect endpoint
|
|
112
135
|
* (reachable only by the runner — never published outward), and relays each
|
|
@@ -131,6 +154,11 @@ export type RunStatus =
|
|
|
131
154
|
*/
|
|
132
155
|
export type RunPhase = "build" | "provision" | "boot";
|
|
133
156
|
|
|
157
|
+
/** Per-port reachability of a running session's declared ports, watched by the
|
|
158
|
+
* runner from its own network. Drives the editor's endpoint badge
|
|
159
|
+
* (spinner → ok / error) instead of an app-log line. */
|
|
160
|
+
export type ReachabilityState = "checking" | "reachable" | "unreachable";
|
|
161
|
+
|
|
134
162
|
export type RunEvent =
|
|
135
163
|
| { type: "stdout"; chunk: string }
|
|
136
164
|
| { type: "stderr"; chunk: string }
|
|
@@ -138,7 +166,9 @@ export type RunEvent =
|
|
|
138
166
|
| { type: "progress"; phase: RunPhase; message: string; done?: boolean }
|
|
139
167
|
/** A frame relayed from the workload's kernel debug stream (kernel event or
|
|
140
168
|
* log line). Only emitted when the session was started with `inspect`. */
|
|
141
|
-
| { type: "debug"; frame: DebugFrame }
|
|
169
|
+
| { type: "debug"; frame: DebugFrame }
|
|
170
|
+
/** A reachability transition for one declared port, keyed by port. */
|
|
171
|
+
| { type: "reachability"; port: number; state: ReachabilityState };
|
|
142
172
|
|
|
143
173
|
export function isTerminal(status: RunStatus): boolean {
|
|
144
174
|
return status.kind === "exited" || status.kind === "failed" || status.kind === "stopped";
|
package/src/debug/relay.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { DebugFrame } from "@telorun/debug-wire";
|
|
2
2
|
|
|
3
|
+
import { abortableDelay } from "../abortable-delay.js";
|
|
4
|
+
|
|
3
5
|
export interface DebugRelayOptions {
|
|
4
6
|
/** The workload's inspect SSE endpoint, e.g. `http://telo-run-<id>:9230/events`.
|
|
5
7
|
* Reachable only by the runner over the backend's private network. */
|
|
@@ -34,7 +36,7 @@ export async function relayDebugStream(opts: DebugRelayOptions): Promise<void> {
|
|
|
34
36
|
headers: { accept: "text/event-stream" },
|
|
35
37
|
});
|
|
36
38
|
if (!res.ok || !res.body) {
|
|
37
|
-
await
|
|
39
|
+
await abortableDelay(RECONNECT_DELAY_MS, signal);
|
|
38
40
|
continue;
|
|
39
41
|
}
|
|
40
42
|
await pump(res.body, onFrame, signal);
|
|
@@ -42,7 +44,7 @@ export async function relayDebugStream(opts: DebugRelayOptions): Promise<void> {
|
|
|
42
44
|
if (signal.aborted) return;
|
|
43
45
|
opts.onError?.(err instanceof Error ? err : new Error(String(err)));
|
|
44
46
|
}
|
|
45
|
-
await
|
|
47
|
+
await abortableDelay(RECONNECT_DELAY_MS, signal);
|
|
46
48
|
}
|
|
47
49
|
}
|
|
48
50
|
|
|
@@ -87,18 +89,3 @@ function parseSseData(block: string): DebugFrame | null {
|
|
|
87
89
|
return null;
|
|
88
90
|
}
|
|
89
91
|
}
|
|
90
|
-
|
|
91
|
-
function delay(ms: number, signal: AbortSignal): Promise<void> {
|
|
92
|
-
return new Promise((resolve) => {
|
|
93
|
-
if (signal.aborted) return resolve();
|
|
94
|
-
const timer = setTimeout(() => {
|
|
95
|
-
signal.removeEventListener("abort", onAbort);
|
|
96
|
-
resolve();
|
|
97
|
-
}, ms);
|
|
98
|
-
const onAbort = (): void => {
|
|
99
|
-
clearTimeout(timer);
|
|
100
|
-
resolve();
|
|
101
|
-
};
|
|
102
|
-
signal.addEventListener("abort", onAbort, { once: true });
|
|
103
|
-
});
|
|
104
|
-
}
|