@telorun/runner-core 0.6.0 → 0.8.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.
@@ -1,8 +1,5 @@
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 { generateSessionId } from "../session/session-id.js";
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
4
  const startBodySchema = {
8
5
  type: "object",
@@ -30,17 +27,7 @@ const startBodySchema = {
30
27
  type: "object",
31
28
  additionalProperties: { type: "string" },
32
29
  },
33
- ports: {
34
- type: "array",
35
- items: {
36
- type: "object",
37
- required: ["port", "protocol"],
38
- properties: {
39
- port: { type: "integer", minimum: 1, maximum: 65535 },
40
- protocol: { type: "string", enum: ["tcp", "udp"] },
41
- },
42
- },
43
- },
30
+ ports: portsSchema,
44
31
  config: {
45
32
  type: "object",
46
33
  required: ["image", "pullPolicy"],
@@ -56,17 +43,8 @@ const startBodySchema = {
56
43
  export function sessionsRoute(deps) {
57
44
  return async (app) => {
58
45
  app.post("/v1/sessions", { schema: { body: startBodySchema } }, async (req, reply) => {
59
- // Terms enforcement — the server is the source of truth, so a client that
60
- // skips the editor gate still can't start a session without acknowledging
61
- // the current terms version.
62
- if (deps.terms) {
63
- const raw = req.headers[ACCEPTED_TERMS_HEADER];
64
- const accepted = Array.isArray(raw) ? raw[0] : raw;
65
- if (accepted !== deps.terms.version) {
66
- reply.code(428).send({ error: "terms_required", terms: deps.terms });
67
- return;
68
- }
69
- }
46
+ if (!enforceTerms(req, reply, deps.terms))
47
+ return;
70
48
  return startSession(app, deps, req.body, reply);
71
49
  });
72
50
  app.get("/v1/sessions/:id", async (req, reply) => {
@@ -111,7 +89,6 @@ export function sessionsRoute(deps) {
111
89
  };
112
90
  }
113
91
  async function startSession(app, deps, body, reply) {
114
- const sessionId = generateSessionId();
115
92
  let entryRelative;
116
93
  try {
117
94
  // Traversal guard for the entry path and every bundle file — a `../foo`
@@ -138,87 +115,14 @@ async function startSession(app, deps, body, reply) {
138
115
  return;
139
116
  }
140
117
  }
141
- let entry;
142
- try {
143
- entry = deps.registry.register({ sessionId });
144
- }
145
- catch (err) {
146
- if (err instanceof SessionLimitError) {
147
- reply.code(409).send({ error: "too_many_sessions", message: err.message });
148
- return;
149
- }
150
- throw err;
151
- }
152
- // Surface a TELO_REGISTRY_URL to the workload so the telo CLI inside picks
153
- // it up. Precedence: body.env explicit value > body.config.registryUrl
154
- // (per-request override) > runner's own default. Trim client-supplied URLs
155
- // so stray whitespace from an editor input doesn't flow into the workload.
156
- const configRegistryUrl = body.config.registryUrl?.trim() || undefined;
157
- const registryUrl = configRegistryUrl ?? deps.defaultRegistryUrl;
158
- const sessionEnv = registryUrl && !("TELO_REGISTRY_URL" in body.env)
159
- ? { ...body.env, TELO_REGISTRY_URL: registryUrl }
160
- : body.env;
161
- // Respond as soon as the session is registered — BEFORE the backend starts.
162
- // `backend.start()` now spans the on-cluster image build and pod bring-up,
163
- // which can take seconds-to-minutes; awaiting it here would hide the event
164
- // stream until the workload is already up, so the client never sees build /
165
- // provision / boot progress live. Returning the streamUrl first lets the
166
- // client connect immediately; start runs in the background and its progress,
167
- // output, and terminal status flow over the stream.
168
- reply.code(201).send({
169
- sessionId,
170
- streamUrl: `/v1/sessions/${sessionId}/events`,
171
- createdAt: entry.createdAt.toISOString(),
172
- });
173
- deps.backend
174
- .start({
175
- sessionId,
118
+ return startWorkloadSession(app, deps, {
176
119
  bundle: body.bundle,
177
120
  entryRelativePath: entryRelative,
178
- env: sessionEnv,
121
+ env: body.env,
179
122
  ports: body.ports ?? [],
180
123
  config: body.config,
124
+ selfContained: false,
181
125
  inspect: body.inspect ?? false,
182
- onStatus: (status) => deps.registry.emit(sessionId, { type: "status", status }),
183
- onProgress: (phase, message, done) => deps.registry.emit(sessionId, { type: "progress", phase, message, done }),
184
- onOutput: (chunk) => deps.registry.pushBytes(sessionId, chunk),
185
- // Relay only kernel *event* frames to the client. stdout/stderr already
186
- // arrive over the byte channel (onOutput), so forwarding log frames would
187
- // double the traffic and let log spam evict lifecycle events from the
188
- // byte-capped replay buffer. The editor discards relayed logs anyway.
189
- onDebug: (frame) => {
190
- if (isEventFrame(frame))
191
- deps.registry.emit(sessionId, { type: "debug", frame });
192
- },
193
- onReachability: (port, state) => deps.registry.emit(sessionId, { type: "reachability", port, state }),
194
- isUserStopped: () => entry.userStopped,
195
- })
196
- .then(async (session) => {
197
- entry.session = session;
198
- // Pre-start DELETE race: a DELETE received during backend.start (e.g.
199
- // while an image build was running) can't stop a workload that didn't
200
- // exist yet — it set userStopped and returned 204. Now that the workload
201
- // is live, honor the earlier DELETE.
202
- if (entry.userStopped) {
203
- try {
204
- await session.stop();
205
- }
206
- catch (err) {
207
- app.log.warn({ err, sessionId }, "failed to stop after race with pre-start DELETE");
208
- }
209
- }
210
- })
211
- .catch((err) => {
212
- // The 201 is already sent, so a start failure surfaces as a terminal
213
- // `failed` status on the stream (the registry schedules eviction on a
214
- // terminal status; the SSE channel delivers it, then closes).
215
- const message = err instanceof SessionStartError
216
- ? `${err.stage}: ${err.message}`
217
- : err instanceof Error
218
- ? err.message
219
- : String(err);
220
- app.log.error({ err, sessionId }, "session start failed");
221
- deps.registry.emit(sessionId, { type: "status", status: { kind: "failed", message } });
222
- });
126
+ }, reply);
223
127
  }
224
128
  //# sourceMappingURL=sessions.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"sessions.js","sourceRoot":"","sources":["../../src/routes/sessions.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"sessions.js","sourceRoot":"","sources":["../../src/routes/sessions.ts"],"names":[],"mappings":"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"}
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,14 @@ 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; sessions of them are created via
25
+ * `POST /v1/apps/:name/sessions`. */
26
+ apps?: Record<string, ResolvedRunnerApp>;
21
27
  registry?: SessionRegistry;
22
28
  }
23
29
  export interface ServerHandle {
@@ -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;AACpD,OAAO,KAAK,EAAE,kBAAkB,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAMvE,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;iFAC6E;IAC7E,cAAc,CAAC,EAAE,CAAC,MAAM,EAAE,aAAa,KAAK,MAAM,GAAG,SAAS,CAAC;IAC/D,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,CA8CzE;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"}
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";
@@ -25,11 +26,19 @@ export async function buildServer(deps) {
25
26
  exitTtlMs: deps.config.exitTtlMs,
26
27
  replayBufferBytes: deps.config.replayBufferBytes,
27
28
  });
29
+ // The app catalog is injected into the served capabilities document here, so
30
+ // what /v1/capabilities advertises and what the session route accepts can
31
+ // never drift — both come from `deps.apps`.
32
+ const appDescriptors = Object.values(deps.apps ?? {}).map(({ name, title, description }) => ({ name, title, description }));
33
+ const withApps = (caps) => appDescriptors.length > 0 ? { ...caps, apps: appDescriptors } : caps;
34
+ const capabilitiesGetter = typeof deps.capabilities === "function"
35
+ ? () => withApps(deps.capabilities())
36
+ : withApps(deps.capabilities);
28
37
  // Terms are stable across the process — resolve the capabilities once for them
29
38
  // even when `capabilities` is a getter (the route still re-resolves per request).
30
- const capabilitiesValue = typeof deps.capabilities === "function" ? deps.capabilities() : deps.capabilities;
39
+ const capabilitiesValue = typeof capabilitiesGetter === "function" ? capabilitiesGetter() : capabilitiesGetter;
31
40
  await app.register(healthRoute(deps.version));
32
- await app.register(capabilitiesRoute(deps.capabilities));
41
+ await app.register(capabilitiesRoute(capabilitiesGetter));
33
42
  await app.register(probeRoute({ backend: deps.backend }));
34
43
  await app.register(sessionsRoute({
35
44
  backend: deps.backend,
@@ -38,9 +47,16 @@ export async function buildServer(deps) {
38
47
  defaultRegistryUrl: deps.defaultRegistryUrl,
39
48
  validateConfig: deps.validateConfig,
40
49
  // The capabilities document is the single source of the runner's terms;
41
- // the session route enforces what /v1/capabilities advertises.
50
+ // every session-creating route enforces what /v1/capabilities advertises.
42
51
  terms: capabilitiesValue.terms,
43
52
  }));
53
+ await app.register(appsRoute({
54
+ backend: deps.backend,
55
+ registry,
56
+ defaultRegistryUrl: deps.defaultRegistryUrl,
57
+ terms: capabilitiesValue.terms,
58
+ apps: deps.apps,
59
+ }));
44
60
  await app.register(ioRoute({ registry, corsOrigins: deps.config.corsOrigins }));
45
61
  return { app, registry };
46
62
  }
@@ -1 +1 @@
1
- {"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"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"}
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.6.0",
3
+ "version": "0.8.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,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
@@ -48,6 +48,11 @@ 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 (`POST /v1/apps/:name/sessions`):
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;
51
56
  /** When true, launch the workload with `--inspect` and relay its kernel debug
52
57
  * stream via `onDebug`. The inspect endpoint stays reachable only by the
53
58
  * runner — never published outward. */
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
+ * (`POST /v1/apps/:name/sessions`). Absent/empty when none are offered —
34
+ * clients 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: an app session is created via `POST /v1/apps/<name>/sessions`. */
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>;
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
+ }