@velum-labs/routekit-daemon 1.2.0 → 1.3.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/README.md CHANGED
@@ -17,3 +17,11 @@ primary and `runRouteKitDaemonWorker` in cluster workers.
17
17
  Applications normally use it through `@velum-labs/routekit`. Tests and specialized
18
18
  hosts can acquire a scoped gateway generation from `@velum-labs/routekit-daemon/effect`
19
19
  without claiming the singleton service record.
20
+
21
+ `launcher.prepare` discovers a named manifest upward from the requested launch
22
+ working directory and returns a memory-only gateway credential bound to that
23
+ repository/profile pair. The singleton proxy projects that trusted context to
24
+ the read-only compositional policy reader per request; it never derives a
25
+ repository from the daemon-global router config or accepts an untrusted path
26
+ header. Selecting one never writes the repository, durable token store,
27
+ daemon-global config, or daemon-global activation store.
@@ -31,6 +31,7 @@ import { DaemonState } from "../daemon-state-context.js";
31
31
  import { DataPlane } from "../data-plane-context.js";
32
32
  import { EvalSessions } from "../services/eval-session/service.js";
33
33
  import { Generations } from "../services/generations/service.js";
34
+ import { LauncherSessions } from "../services/launcher-session/service.js";
34
35
  import { Leaderboard } from "../leaderboard-context.js";
35
36
  import { Sidecar } from "../sidecar-context.js";
36
37
  import { Telemetry } from "../services/telemetry/service.js";
@@ -42,9 +43,7 @@ class DaemonFoundation extends Context.Service()("@velum-labs/routekit-daemon/Da
42
43
  const prepareFoundation = (options) => Effect.acquireRelease(Effect.tryPromise({
43
44
  try: async () => {
44
45
  const preflight = await prepareDaemonBootstrap(options);
45
- const previous = preflight.hosted === undefined
46
- ? preflight.store.read(ROUTEKIT_DAEMON_KIND)
47
- : undefined;
46
+ const previous = preflight.hosted === undefined ? preflight.store.read(ROUTEKIT_DAEMON_KIND) : undefined;
48
47
  if (preflight.hosted === undefined &&
49
48
  previous !== undefined &&
50
49
  previous.pid !== process.pid &&
@@ -112,6 +111,7 @@ const acquireRunningDaemon = Effect.fn("Daemon.acquireRunning")(function* (optio
112
111
  const generations = yield* Generations;
113
112
  const gateway = yield* ActiveGateway;
114
113
  const evalSessions = yield* EvalSessions;
114
+ const launcherSessions = yield* LauncherSessions;
115
115
  const tokens = yield* Tokens;
116
116
  const telemetry = yield* Telemetry;
117
117
  const auth = yield* AccountAuth;
@@ -134,6 +134,9 @@ const acquireRunningDaemon = Effect.fn("Daemon.acquireRunning")(function* (optio
134
134
  port: options.port ?? 8080,
135
135
  authToken: tokens.dataAuth.token,
136
136
  resolveDataPrincipal: (presented) => {
137
+ const launcherPrincipal = launcherSessions.resolve(presented);
138
+ if (launcherPrincipal !== undefined)
139
+ return launcherPrincipal;
137
140
  const evalPrincipal = evalSessions.resolve(presented);
138
141
  if (evalPrincipal !== undefined)
139
142
  return evalPrincipal;
@@ -146,7 +149,11 @@ const acquireRunningDaemon = Effect.fn("Daemon.acquireRunning")(function* (optio
146
149
  yield* Effect.addFinalizer(() => proxy.drain(foundation.drainGraceMs).pipe(Effect.ignore));
147
150
  const portless = foundation.hosted === undefined
148
151
  ? yield* Effect.acquireRelease(Effect.tryPromise({
149
- try: () => createPortlessSession(options.portless ?? foundation.env.ROUTEKIT_PORTLESS !== "0", { project: ROUTEKIT_PRODUCT, ownerLabel: "routekit-daemon", bareNames: [] }),
152
+ try: () => createPortlessSession(options.portless ?? foundation.env.ROUTEKIT_PORTLESS !== "0", {
153
+ project: ROUTEKIT_PRODUCT,
154
+ ownerLabel: "routekit-daemon",
155
+ bareNames: []
156
+ }),
150
157
  catch: toRouteKitFailure
151
158
  }), (session) => Effect.sync(() => {
152
159
  if (session.enabled)
@@ -265,7 +272,10 @@ const acquireRunningDaemon = Effect.fn("Daemon.acquireRunning")(function* (optio
265
272
  });
266
273
  const reload = Effect.gen(function* () {
267
274
  const document = canonicalConfigDocument(foundation.configPath);
268
- yield* generations.replace(foundation.runtimeState.config, document, { write: false, configRevision: true });
275
+ yield* generations.replace(foundation.runtimeState.config, document, {
276
+ write: false,
277
+ configRevision: true
278
+ });
269
279
  });
270
280
  return DaemonRuntime.of({
271
281
  record,
@@ -292,9 +302,7 @@ export function daemonLive(options) {
292
302
  : createHostedCliproxySidecar(value.hosted.sidecarRequest)), (sidecar) => sidecar.close.pipe(Effect.ignore)))), Layer.unwrap(Effect.map(DaemonFoundation, (value) => Tokens.layer({
293
303
  home: value.home,
294
304
  ...(options.authToken === undefined ? {} : { authToken: options.authToken }),
295
- ...(options.authTokenFile === undefined
296
- ? {}
297
- : { authTokenFile: options.authTokenFile })
305
+ ...(options.authTokenFile === undefined ? {} : { authTokenFile: options.authTokenFile })
298
306
  }))), Layer.unwrap(Effect.map(DaemonFoundation, (value) => Telemetry.layer({
299
307
  home: value.home,
300
308
  env: value.env,
@@ -319,7 +327,7 @@ export function daemonLive(options) {
319
327
  });
320
328
  }
321
329
  });
322
- }), (leaderboard) => Effect.sync(() => leaderboard.rollups.flush())))), EvalSessions.layer()).pipe(Layer.provideMerge(platform));
330
+ }), (leaderboard) => Effect.sync(() => leaderboard.rollups.flush())))), EvalSessions.layer(), LauncherSessions.layer()).pipe(Layer.provideMerge(platform));
323
331
  const staticServices = Layer.mergeAll(Layer.effect(DaemonEnv, Effect.map(DaemonFoundation, (value) => DaemonEnv.of({
324
332
  home: value.home,
325
333
  configPath: value.configPath,
@@ -18,6 +18,7 @@ export { DaemonState } from "./daemon-state-context.js";
18
18
  export type { DataPlaneValue } from "./data-plane-context.js";
19
19
  export { DataPlane } from "./data-plane-context.js";
20
20
  export { EvalSessions } from "./services/eval-session/service.js";
21
+ export { LauncherSessions } from "./services/launcher-session/service.js";
21
22
  export type { GatewayGenerationOptions, GatewayGenerationRedeemResetOptions, GatewayGenerationRedeemResetResponse, RunningGatewayGeneration } from "./services/gateway-generation/service.js";
22
23
  export { startGatewayGenerationEffect } from "./services/gateway-generation/service.js";
23
24
  export type { DaemonGenerationHooks } from "./services/generations/service.js";
@@ -10,6 +10,7 @@ export { DaemonPolicy } from "./daemon-policy-context.js";
10
10
  export { DaemonState } from "./daemon-state-context.js";
11
11
  export { DataPlane } from "./data-plane-context.js";
12
12
  export { EvalSessions } from "./services/eval-session/service.js";
13
+ export { LauncherSessions } from "./services/launcher-session/service.js";
13
14
  export { startGatewayGenerationEffect } from "./services/gateway-generation/service.js";
14
15
  export { Generations } from "./services/generations/service.js";
15
16
  export { Leaderboard } from "./leaderboard-context.js";
@@ -2,4 +2,4 @@ import { type CompositionalRoutingPolicyReader } from "@velum-labs/routekit-gate
2
2
  /** Daemon-owned location for the compact policy artifact consumed online. */
3
3
  export declare function evalRoutingSnapshotDirectory(routekitHome: string): string;
4
4
  /** Read the authoritative routing activation without restarting router generations. */
5
- export declare function makeCompositionalRoutingPolicyReader(routekitHome: string): CompositionalRoutingPolicyReader;
5
+ export declare function makeCompositionalRoutingPolicyReader(routekitHome: string, defaultRepositoryRoot?: string): CompositionalRoutingPolicyReader;
@@ -1,21 +1,101 @@
1
1
  import { join } from "node:path";
2
+ import { loadRepositoryRoutingProfile, ROUTING_PROFILE_DIRECTORY } from "@velum-labs/routekit-config";
3
+ import { AnyPublishedRoutingActivation, assertPublishedRoutingActivation, assertPublishedRoutingActivationV3 } from "@velum-labs/routekit-eval-contracts";
2
4
  import { makeRoutingActivationStore } from "@velum-labs/routekit-eval-store/effect";
3
5
  import { RoutingPolicyReadError } from "@velum-labs/routekit-gateway";
4
- import { Effect } from "effect";
6
+ import { Effect, FileSystem, Schema } from "effect";
5
7
  /** Daemon-owned location for the compact policy artifact consumed online. */
6
8
  export function evalRoutingSnapshotDirectory(routekitHome) {
7
9
  return join(routekitHome, "eval");
8
10
  }
9
11
  /** Read the authoritative routing activation without restarting router generations. */
10
- export function makeCompositionalRoutingPolicyReader(routekitHome) {
12
+ export function makeCompositionalRoutingPolicyReader(routekitHome, defaultRepositoryRoot) {
11
13
  const snapshots = makeRoutingActivationStore(evalRoutingSnapshotDirectory(routekitHome));
12
14
  return {
13
- getActivation: () => snapshots.readDeployment().pipe(Effect.map((deployment) => deployment.authoritative?.activation), Effect.catch((currentCause) => snapshots
14
- .readPrevious()
15
- .pipe(Effect.flatMap((previous) => previous === undefined ? Effect.fail(currentCause) : Effect.succeed(previous)))), Effect.mapError((cause) => new RoutingPolicyReadError({
16
- profileId: "*",
17
- message: "failed to read the authoritative routing activation",
18
- cause
19
- })))
15
+ getActivation: (profileName, requestRepositoryRoot) => {
16
+ const repositoryRoot = requestRepositoryRoot ?? defaultRepositoryRoot;
17
+ return profileName === undefined
18
+ ? Effect.gen(function* () {
19
+ if (repositoryRoot !== undefined) {
20
+ const fs = yield* FileSystem.FileSystem;
21
+ if (yield* fs.exists(join(repositoryRoot, ROUTING_PROFILE_DIRECTORY))) {
22
+ return yield* new RoutingPolicyReadError({
23
+ profileId: "*",
24
+ message: "repository routing profiles require an explicit profile selector"
25
+ });
26
+ }
27
+ }
28
+ return yield* snapshots.readDeployment().pipe(Effect.map((deployment) => deployment.authoritative?.activation), Effect.catch((currentCause) => snapshots
29
+ .readPrevious()
30
+ .pipe(Effect.flatMap((previous) => previous === undefined ? Effect.fail(currentCause) : Effect.succeed(previous)))), Effect.mapError((cause) => new RoutingPolicyReadError({
31
+ profileId: "*",
32
+ message: "failed to read the authoritative routing activation",
33
+ cause
34
+ })));
35
+ }).pipe(Effect.mapError((cause) => cause instanceof RoutingPolicyReadError
36
+ ? cause
37
+ : new RoutingPolicyReadError({
38
+ profileId: "*",
39
+ message: "failed to inspect repository routing profiles",
40
+ cause
41
+ })))
42
+ : Effect.gen(function* () {
43
+ if (repositoryRoot === undefined) {
44
+ return yield* new RoutingPolicyReadError({
45
+ profileId: profileName,
46
+ message: "named routing profiles require trusted repository context from launcher preparation or the host policy reader"
47
+ });
48
+ }
49
+ const loaded = yield* Effect.try({
50
+ try: () => loadRepositoryRoutingProfile({
51
+ repositoryRoot,
52
+ name: profileName
53
+ }),
54
+ catch: (cause) => new RoutingPolicyReadError({
55
+ profileId: profileName,
56
+ message: `failed to resolve repository routing profile ${JSON.stringify(profileName)}`,
57
+ cause
58
+ })
59
+ });
60
+ const fs = yield* FileSystem.FileSystem;
61
+ const raw = yield* fs.readFileString(loaded.activationPath).pipe(Effect.mapError((cause) => new RoutingPolicyReadError({
62
+ profileId: profileName,
63
+ message: `failed to read repository routing profile activation ${JSON.stringify(loaded.activationPath)}`,
64
+ cause
65
+ })));
66
+ const json = yield* Effect.try({
67
+ try: () => JSON.parse(raw),
68
+ catch: (cause) => new RoutingPolicyReadError({
69
+ profileId: profileName,
70
+ message: `repository routing profile activation is not valid JSON`,
71
+ cause
72
+ })
73
+ });
74
+ const activation = yield* Schema.decodeUnknownEffect(AnyPublishedRoutingActivation)(json).pipe(Effect.mapError((cause) => new RoutingPolicyReadError({
75
+ profileId: profileName,
76
+ message: "repository routing profile activation has the wrong shape",
77
+ cause
78
+ })));
79
+ yield* Effect.try({
80
+ try: () => {
81
+ if (activation.version === 2) {
82
+ assertPublishedRoutingActivation(activation);
83
+ }
84
+ else {
85
+ assertPublishedRoutingActivationV3(activation);
86
+ }
87
+ if (JSON.stringify(activation.objective) !== JSON.stringify(loaded.profile.objective)) {
88
+ throw new Error("routing profile objective does not match its activation objective");
89
+ }
90
+ },
91
+ catch: (cause) => new RoutingPolicyReadError({
92
+ profileId: profileName,
93
+ message: "repository routing profile activation is invalid",
94
+ cause
95
+ })
96
+ });
97
+ return activation;
98
+ });
99
+ }
20
100
  };
21
101
  }
@@ -109,10 +109,12 @@ export class EvalRoutingApplicationService {
109
109
  })),
110
110
  "evalRouting.installAuthoritative": (params) => Effect.gen(function* () {
111
111
  const env = yield* DaemonEnv;
112
- const activation = yield* decodeAnyActivation(params.activation);
112
+ const activation = yield* makeRoutingActivationStore(evalRoutingSnapshotDirectory(env.home))
113
+ .readActivation(params.activationPath)
114
+ .pipe(Effect.mapError(mapStoreError("read authoritative routing policy")));
113
115
  yield* validateRuntimeCompatibility(activation);
114
116
  const deployment = yield* makeRoutingActivationStore(evalRoutingSnapshotDirectory(env.home))
115
- .installAuthoritative(activation, params.expectedAuthoritativeRevisionDigest, params.expectedPreviousAuthoritativeRevisionDigest)
117
+ .installAuthoritative(activation, params.activationPath, params.expectedAuthoritativeRevisionDigest, params.expectedPreviousAuthoritativeRevisionDigest)
116
118
  .pipe(Effect.mapError(mapStoreError("install authoritative routing policy")));
117
119
  return {
118
120
  installed: true,
@@ -1,7 +1,10 @@
1
+ import { resolve } from "node:path";
2
+ import { findRepositoryRoutingProfileRoot, loadRepositoryRoutingProfile } from "@velum-labs/routekit-config";
1
3
  import { resolveCodexStartupModel } from "@velum-labs/routekit-gateway";
2
4
  import { ControlError } from "@velum-labs/routekit-runtime/control";
3
5
  import { Effect } from "effect";
4
6
  import { ActiveGateway } from "./services/active-gateway/service.js";
7
+ import { LauncherSessions } from "./services/launcher-session/service.js";
5
8
  import { Tokens } from "./services/tokens/service.js";
6
9
  /** Owns coding-tool launch preparation against the live catalog. */
7
10
  export class LauncherApplicationService {
@@ -18,9 +21,60 @@ export class LauncherApplicationService {
18
21
  requestId: "internal"
19
22
  });
20
23
  return yield* Effect.gen(function* () {
21
- let model = params.model ?? listed.defaultModel ?? listed.models[0]?.id;
24
+ const requestedModel = params.model?.trim();
25
+ const modelProfile = requestedModel?.toLowerCase().startsWith("auto:")
26
+ ? requestedModel.slice("auto:".length)
27
+ : undefined;
28
+ const routingProfile = params.routingProfile ?? modelProfile;
29
+ if (params.routingProfile !== undefined && modelProfile !== undefined) {
30
+ if (params.routingProfile !== modelProfile) {
31
+ return yield* Effect.fail(new ControlError({
32
+ code: "bad_request",
33
+ message: "routingProfile conflicts with the model profile selector"
34
+ }));
35
+ }
36
+ }
37
+ else if (params.routingProfile !== undefined &&
38
+ requestedModel !== undefined &&
39
+ requestedModel.toLowerCase() !== "auto") {
40
+ return yield* Effect.fail(new ControlError({
41
+ code: "bad_request",
42
+ message: "routingProfile may be used only with model auto or an omitted model"
43
+ }));
44
+ }
45
+ let launcherAuthToken;
46
+ if (routingProfile !== undefined) {
47
+ const cwd = resolve(params.cwd ?? ".");
48
+ const repositoryRoot = yield* Effect.try({
49
+ try: () => {
50
+ const root = findRepositoryRoutingProfileRoot(cwd, routingProfile);
51
+ if (root === undefined) {
52
+ throw new Error(`routing profile ${JSON.stringify(routingProfile)} was not found from ${cwd}`);
53
+ }
54
+ loadRepositoryRoutingProfile({ repositoryRoot: root, name: routingProfile });
55
+ return root;
56
+ },
57
+ catch: (cause) => new ControlError({
58
+ code: "not_found",
59
+ message: cause instanceof Error
60
+ ? cause.message
61
+ : `routing profile ${JSON.stringify(routingProfile)} was not found`
62
+ })
63
+ });
64
+ const launcherSessions = yield* LauncherSessions;
65
+ launcherAuthToken = launcherSessions.open({
66
+ repositoryRoot,
67
+ profileName: routingProfile,
68
+ role: context.principal === undefined || context.principal.role === "ephemeral"
69
+ ? "owner"
70
+ : context.principal.role
71
+ });
72
+ }
73
+ let model = routingProfile === undefined
74
+ ? (params.model ?? listed.defaultModel ?? listed.models[0]?.id)
75
+ : `auto:${routingProfile}`;
22
76
  let codexSelection;
23
- if (params.tool === "codex") {
77
+ if (params.tool === "codex" && routingProfile === undefined) {
24
78
  const gateway = yield* ActiveGateway;
25
79
  const candidates = listed.models.flatMap((entry) => {
26
80
  const info = gateway.router().modelInfo(entry.id);
@@ -68,7 +122,8 @@ export class LauncherApplicationService {
68
122
  models: [...selected.models]
69
123
  };
70
124
  }
71
- if (model === undefined || !listed.models.some((entry) => entry.id === model)) {
125
+ if (model === undefined ||
126
+ (routingProfile === undefined && !listed.models.some((entry) => entry.id === model))) {
72
127
  return yield* Effect.fail(new ControlError({
73
128
  code: "not_found",
74
129
  message: params.model === undefined
@@ -78,7 +133,7 @@ export class LauncherApplicationService {
78
133
  }
79
134
  const gateway = yield* ActiveGateway;
80
135
  const tokens = yield* Tokens;
81
- const authToken = yield* tokens.dataTokenForPrincipal(context.principal);
136
+ const authToken = launcherAuthToken ?? (yield* tokens.dataTokenForPrincipal(context.principal));
82
137
  return {
83
138
  tool: params.tool,
84
139
  model,
@@ -0,0 +1,23 @@
1
+ import type { GatewayPrincipal } from "@velum-labs/routekit-gateway";
2
+ import { Context, Layer } from "effect";
3
+ export type OpenLauncherSessionInput = {
4
+ repositoryRoot: string;
5
+ profileName: string;
6
+ role: "owner" | "admin";
7
+ };
8
+ export type LauncherSessionManagerOptions = {
9
+ random?: (bytes: number) => Buffer;
10
+ };
11
+ /** Daemon-owned, memory-only credentials that bind one launcher to one repository profile. */
12
+ export declare class LauncherSessionManager {
13
+ #private;
14
+ constructor(options?: LauncherSessionManagerOptions);
15
+ open(input: OpenLauncherSessionInput): string;
16
+ resolve(presented: string): GatewayPrincipal | undefined;
17
+ closeAll(): void;
18
+ }
19
+ declare const LauncherSessions_base: Context.ServiceClass<LauncherSessions, "@velum-labs/routekit-daemon/LauncherSessions", LauncherSessionManager>;
20
+ export declare class LauncherSessions extends LauncherSessions_base {
21
+ static layer(options?: LauncherSessionManagerOptions): Layer.Layer<LauncherSessions, never, never>;
22
+ }
23
+ export {};
@@ -0,0 +1,48 @@
1
+ import { createHash, randomBytes } from "node:crypto";
2
+ import { Context, Effect, Layer } from "effect";
3
+ function digest(value) {
4
+ return createHash("sha256").update(value, "utf8").digest("hex");
5
+ }
6
+ /** Daemon-owned, memory-only credentials that bind one launcher to one repository profile. */
7
+ export class LauncherSessionManager {
8
+ #sessionsByTokenDigest = new Map();
9
+ #random;
10
+ constructor(options = {}) {
11
+ this.#random = options.random ?? randomBytes;
12
+ }
13
+ open(input) {
14
+ const credential = this.#random(32).toString("base64url");
15
+ const tokenDigest = digest(credential);
16
+ const session = {
17
+ id: `launcher_${this.#random(16).toString("base64url")}`,
18
+ tokenDigest,
19
+ repositoryRoot: input.repositoryRoot,
20
+ profileName: input.profileName,
21
+ role: input.role
22
+ };
23
+ this.#sessionsByTokenDigest.set(tokenDigest, session);
24
+ return credential;
25
+ }
26
+ resolve(presented) {
27
+ const session = this.#sessionsByTokenDigest.get(digest(presented));
28
+ if (session === undefined)
29
+ return undefined;
30
+ return {
31
+ id: session.id,
32
+ label: `launcher:${session.profileName}`,
33
+ role: session.role,
34
+ routing: {
35
+ repositoryRoot: session.repositoryRoot,
36
+ profileName: session.profileName
37
+ }
38
+ };
39
+ }
40
+ closeAll() {
41
+ this.#sessionsByTokenDigest.clear();
42
+ }
43
+ }
44
+ export class LauncherSessions extends Context.Service()("@velum-labs/routekit-daemon/LauncherSessions") {
45
+ static layer(options = {}) {
46
+ return Layer.effect(LauncherSessions, Effect.acquireRelease(Effect.sync(() => new LauncherSessionManager(options)), (sessions) => Effect.sync(() => sessions.closeAll())));
47
+ }
48
+ }
@@ -9,6 +9,7 @@ import { DaemonLifecycleService } from "../daemon-lifecycle-service.js";
9
9
  import { DoctorApplicationService } from "../doctor-service.js";
10
10
  import { EvalRoutingApplicationService } from "../eval-routing-service.js";
11
11
  import { EvalSessionApplicationService } from "../services/eval-session/service.js";
12
+ import { LauncherSessionManager } from "../services/launcher-session/service.js";
12
13
  import { LauncherApplicationService } from "../launcher-service.js";
13
14
  import { ProviderQueryService } from "../provider-query-service.js";
14
15
  import { RouterGenerationService } from "../router-generation-service.js";
@@ -109,3 +110,26 @@ test("application services expose concrete bounded handler groups", () => {
109
110
  "telemetry.set"
110
111
  ]);
111
112
  });
113
+ test("launcher sessions bind repository profile context without durable token state", () => {
114
+ let byte = 0;
115
+ const sessions = new LauncherSessionManager({
116
+ random: (length) => Buffer.alloc(length, byte++)
117
+ });
118
+ const token = sessions.open({
119
+ repositoryRoot: "/workspace/repository",
120
+ profileName: "quality",
121
+ role: "owner"
122
+ });
123
+ assert.deepEqual(sessions.resolve(token), {
124
+ id: `launcher_${Buffer.alloc(16, 1).toString("base64url")}`,
125
+ label: "launcher:quality",
126
+ role: "owner",
127
+ routing: {
128
+ repositoryRoot: "/workspace/repository",
129
+ profileName: "quality"
130
+ }
131
+ });
132
+ assert.equal(sessions.resolve("wrong"), undefined);
133
+ sessions.closeAll();
134
+ assert.equal(sessions.resolve(token), undefined);
135
+ });
@@ -1,15 +1,17 @@
1
1
  import assert from "node:assert/strict";
2
- import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
2
+ import { cpSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
3
3
  import { request as httpRequest } from "node:http";
4
4
  import { tmpdir } from "node:os";
5
- import { join } from "node:path";
5
+ import { dirname, join } from "node:path";
6
6
  import test from "node:test";
7
+ import { fileURLToPath } from "node:url";
7
8
  import { RouteKitControlClient } from "@velum-labs/routekit-control";
8
9
  import { ControlClient, ControlError } from "@velum-labs/routekit-runtime/control";
9
10
  import { createServiceRecordStore } from "@velum-labs/routekit-runtime/service";
10
11
  import { runRouteKitEffect, runRouteKitEffectExit, throwRouteKitExit } from "@velum-labs/routekit-runtime/effect";
11
12
  import { startRouteKitDaemon } from "../index.js";
12
13
  import { mockProvider, nativeCredential, withMockNativeDiscovery } from "./daemon-fixtures.js";
14
+ const REPOSITORY_PROFILE_FIXTURE = join(dirname(fileURLToPath(import.meta.url)), "../../../../test/fixtures/repository-routing-profiles");
13
15
  test("broken optional subscription credentials do not block a healthy API provider", async () => {
14
16
  const root = mkdtempSync(join(tmpdir(), "routekit-daemon-broken-subscription-"));
15
17
  const stateHome = join(root, "state");
@@ -17,13 +19,7 @@ test("broken optional subscription credentials do not block a healthy API provid
17
19
  const accountsDirectory = join(stateHome, "subscriptions", "codex");
18
20
  mkdirSync(accountsDirectory, { recursive: true, mode: 0o700 });
19
21
  writeFileSync(join(accountsDirectory, "broken.json"), "{not-json", { mode: 0o600 });
20
- writeFileSync(configPath, [
21
- "providers:",
22
- " openai: {}",
23
- " codex: {}",
24
- "defaultModel: openai/mock-model",
25
- ""
26
- ].join("\n"));
22
+ writeFileSync(configPath, ["providers:", " openai: {}", " codex: {}", "defaultModel: openai/mock-model", ""].join("\n"));
27
23
  const upstream = await mockProvider();
28
24
  let daemon;
29
25
  try {
@@ -739,3 +735,112 @@ test("cleared persisted cooldown remains absent and eligible after daemon reload
739
735
  rmSync(root, { recursive: true, force: true });
740
736
  }
741
737
  });
738
+ test("singleton launcher targets repository profiles without clobbering sibling or global state", async () => {
739
+ const root = mkdtempSync(join(tmpdir(), "routekit-daemon-launcher-profiles-"));
740
+ const stateHome = join(root, "state");
741
+ const repository = join(root, "repository");
742
+ const cwd = join(repository, "packages", "app");
743
+ const configPath = join(root, ".config", "routekit", "router.yaml");
744
+ const globalActivation = join(stateHome, "eval", "published-routing.json");
745
+ cpSync(REPOSITORY_PROFILE_FIXTURE, repository, { recursive: true });
746
+ const classifierDimensions = ["code", "navigation", "debugging", "architecture", "explanation"];
747
+ for (const profile of ["quality", "cost"]) {
748
+ const path = join(repository, ".routekit", "routing", "activations", `${profile}.json`);
749
+ const current = JSON.parse(readFileSync(path, "utf8"));
750
+ current.dimensions = classifierDimensions.map((id) => ({
751
+ id,
752
+ description: `Tasks centered on ${id}`,
753
+ includes: [`Includes ${id}`],
754
+ excludes: [`Excludes work outside ${id}`]
755
+ }));
756
+ current.evidence = current.evidence.map((entry, index) => ({
757
+ ...entry,
758
+ dimensionId: classifierDimensions[index % classifierDimensions.length]
759
+ }));
760
+ writeFileSync(path, `${JSON.stringify(current, null, 2)}\n`);
761
+ }
762
+ mkdirSync(cwd, { recursive: true });
763
+ mkdirSync(dirname(configPath), { recursive: true });
764
+ mkdirSync(dirname(globalActivation), { recursive: true });
765
+ writeFileSync(configPath, "providers:\n openai: {}\ndefaultModel: openai/quality-model\nclassifierModel: openai/gpt-5.6-luna\n");
766
+ writeFileSync(globalActivation, '{"global":"unchanged"}\n');
767
+ const upstream = await mockProvider([
768
+ { id: "quality-model", object: "model" },
769
+ { id: "cost-model", object: "model" },
770
+ { id: "gpt-5.6-luna", object: "model" }
771
+ ]);
772
+ let daemon;
773
+ try {
774
+ daemon = await startRouteKitDaemon({
775
+ packageVersion: "1.2.3",
776
+ stateHome,
777
+ configPath,
778
+ port: 0,
779
+ portless: false,
780
+ env: {
781
+ HOME: root,
782
+ ROUTEKIT_HOME: stateHome,
783
+ OPENAI_API_KEY: "test-key",
784
+ OPENAI_BASE_URL: upstream.url,
785
+ ROUTEKIT_PORTLESS: "0"
786
+ }
787
+ });
788
+ const client = new RouteKitControlClient({
789
+ url: daemon.record.url,
790
+ token: daemon.record.controlToken
791
+ });
792
+ const qualityProfile = join(repository, ".routekit", "routing", "profiles", "quality.yaml");
793
+ const costProfile = join(repository, ".routekit", "routing", "profiles", "cost.yaml");
794
+ const qualityActivation = join(repository, ".routekit", "routing", "activations", "quality.json");
795
+ const costActivation = join(repository, ".routekit", "routing", "activations", "cost.json");
796
+ const tracked = [
797
+ qualityProfile,
798
+ costProfile,
799
+ qualityActivation,
800
+ costActivation,
801
+ configPath,
802
+ globalActivation,
803
+ join(stateHome, "secrets", "tokens.json")
804
+ ];
805
+ const before = new Map(tracked.map((path) => [path, readFileSync(path, "utf8")]));
806
+ const target = async (name, expectedModel, selector) => {
807
+ const prepared = await runRouteKitEffect(client.call("launcher.prepare", {
808
+ tool: "codex",
809
+ ...(selector === "model"
810
+ ? { model: `auto:${name}` }
811
+ : { model: "auto", routingProfile: name }),
812
+ cwd
813
+ }));
814
+ assert.equal(prepared.model, `auto:${name}`);
815
+ const response = await fetch(`${prepared.gatewayUrl}/v1/chat/completions`, {
816
+ method: "POST",
817
+ headers: {
818
+ authorization: `Bearer ${prepared.authToken}`,
819
+ "content-type": "application/json",
820
+ ...(selector === "profile" ? { "x-routekit-routing-profile": name } : {})
821
+ },
822
+ body: JSON.stringify({
823
+ model: selector === "model" ? `auto:${name}` : "auto",
824
+ messages: [{ role: "user", content: "Implement this code change" }]
825
+ })
826
+ });
827
+ const body = await response.text();
828
+ assert.equal(response.status, 200, body);
829
+ assert.match(body, new RegExp(expectedModel));
830
+ };
831
+ await target("quality", "quality-model", "model");
832
+ for (const path of [costProfile, costActivation, configPath, globalActivation]) {
833
+ assert.equal(readFileSync(path, "utf8"), before.get(path), `${path} changed`);
834
+ }
835
+ await target("cost", "cost-model", "profile");
836
+ for (const path of tracked) {
837
+ assert.equal(readFileSync(path, "utf8"), before.get(path), `${path} changed`);
838
+ }
839
+ }
840
+ finally {
841
+ if (daemon !== undefined)
842
+ await daemon.close();
843
+ await upstream.close();
844
+ rmSync(root, { recursive: true, force: true });
845
+ }
846
+ });
@@ -46,6 +46,15 @@ async function mockProvider(models = [
46
46
  const send = () => {
47
47
  res.setHeader("content-type", "application/json");
48
48
  res.end(JSON.stringify({
49
+ model: (() => {
50
+ try {
51
+ const body = JSON.parse(Buffer.concat(chunks).toString("utf8"));
52
+ return typeof body.model === "string" ? body.model : undefined;
53
+ }
54
+ catch {
55
+ return undefined;
56
+ }
57
+ })(),
49
58
  choices: [
50
59
  {
51
60
  index: 0,
@@ -1,5 +1,5 @@
1
1
  import assert from "node:assert/strict";
2
- import { mkdtempSync, readFileSync, rmSync } from "node:fs";
2
+ import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
3
3
  import { tmpdir } from "node:os";
4
4
  import { join } from "node:path";
5
5
  import test from "node:test";
@@ -83,6 +83,7 @@ test("legacy V2 publication is rejected because V2 is rollback-only", async () =
83
83
  });
84
84
  test("V3 authority installs when its pinned models match the running gateway", async () => {
85
85
  const home = mkdtempSync(join(tmpdir(), "routekit-routing-v3-authority-"));
86
+ const repositoryRoot = mkdtempSync(join(tmpdir(), "routekit-routing-v3-repository-"));
86
87
  const activationV3 = JSON.parse(readFileSync(new URL("../../../../test/fixtures/routing-v3/examples/published-routing-activation-v3.example.json", import.meta.url), "utf8"));
87
88
  const daemonEnv = Layer.succeed(DaemonEnv, DaemonEnv.of({
88
89
  home,
@@ -124,17 +125,27 @@ test("V3 authority installs when its pinned models match the running gateway", a
124
125
  const services = Layer.mergeAll(daemonEnv, DaemonState.layer(runtimeState), activeGateway);
125
126
  const handler = new EvalRoutingApplicationService().handlers()["evalRouting.installAuthoritative"];
126
127
  const run = (effect) => runRouteKitEffect(effect.pipe(Effect.provide(services)));
128
+ const activationPath = join(repositoryRoot, ".routekit", "routing", "activations", `${activationV3.activationDigest}.json`);
129
+ mkdirSync(join(repositoryRoot, ".routekit", "routing", "activations"), {
130
+ recursive: true
131
+ });
132
+ writeFileSync(activationPath, `${JSON.stringify(activationV3, null, 2)}\n`);
127
133
  const params = {
128
134
  expectedAuthoritativeRevisionDigest: null,
129
135
  expectedPreviousAuthoritativeRevisionDigest: null,
130
- activation: activationV3
136
+ activationPath
131
137
  };
132
138
  try {
133
139
  const installed = await run(handler(params, undefined));
134
140
  assert.equal(installed.installed, true);
135
141
  assert.equal(installed.authoritative.version, 3);
142
+ const pointer = readFileSync(join(home, "eval", "routing-deployment.v2.json"), "utf8");
143
+ assert.match(pointer, /"activationPath"/u);
144
+ assert.equal(pointer.includes('"activation":'), false);
145
+ assert.equal(pointer.includes('"basis":'), false);
136
146
  }
137
147
  finally {
138
148
  rmSync(home, { recursive: true, force: true });
149
+ rmSync(repositoryRoot, { recursive: true, force: true });
139
150
  }
140
151
  });
@@ -1,11 +1,13 @@
1
1
  import assert from "node:assert/strict";
2
- import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
2
+ import { cpSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
3
3
  import { tmpdir } from "node:os";
4
- import { join } from "node:path";
4
+ import { dirname, join, resolve } from "node:path";
5
5
  import test from "node:test";
6
+ import { fileURLToPath } from "node:url";
6
7
  import { makeRoutingActivationStore } from "@velum-labs/routekit-eval-store/effect";
7
8
  import { runRouteKitEffect } from "@velum-labs/routekit-runtime/effect";
8
9
  import { evalRoutingSnapshotDirectory, makeCompositionalRoutingPolicyReader } from "../eval-routing-policy.js";
10
+ const REPOSITORY_PROFILE_FIXTURE = resolve(dirname(fileURLToPath(import.meta.url)), "../../../../test/fixtures/repository-routing-profiles");
9
11
  test("daemon compositional reader projects authority and falls back to previous V2", async () => {
10
12
  const home = mkdtempSync(join(tmpdir(), "routekit-daemon-compositional-policy-"));
11
13
  const dimensions = [
@@ -53,3 +55,53 @@ test("daemon compositional reader projects authority and falls back to previous
53
55
  rmSync(home, { recursive: true, force: true });
54
56
  }
55
57
  });
58
+ test("two repository routing profiles can be targeted without clobbering each other or daemon-global state", async () => {
59
+ const root = mkdtempSync(join(tmpdir(), "routekit-daemon-repository-profiles-"));
60
+ const repository = join(root, "repository");
61
+ const home = join(root, "state");
62
+ const globalConfig = join(root, "home", ".config", "routekit", "router.yaml");
63
+ try {
64
+ cpSync(REPOSITORY_PROFILE_FIXTURE, repository, { recursive: true });
65
+ mkdirSync(dirname(globalConfig), { recursive: true });
66
+ writeFileSync(globalConfig, "providers:\n openai: {}\ndefaultModel: openai/global-model\n");
67
+ const globalActivation = join(evalRoutingSnapshotDirectory(home), "published-routing.json");
68
+ mkdirSync(dirname(globalActivation), { recursive: true });
69
+ writeFileSync(globalActivation, '{"global":"unchanged"}\n');
70
+ const qualityProfile = join(repository, ".routekit", "routing", "profiles", "quality.yaml");
71
+ const costProfile = join(repository, ".routekit", "routing", "profiles", "cost.yaml");
72
+ const qualityActivation = join(repository, ".routekit", "routing", "activations", "quality.json");
73
+ const costActivation = join(repository, ".routekit", "routing", "activations", "cost.json");
74
+ const paths = [
75
+ qualityProfile,
76
+ costProfile,
77
+ qualityActivation,
78
+ costActivation,
79
+ globalConfig,
80
+ globalActivation
81
+ ];
82
+ const before = new Map(paths.map((path) => [path, readFileSync(path, "utf8")]));
83
+ const reader = makeCompositionalRoutingPolicyReader(home, repository);
84
+ const quality = await runRouteKitEffect(reader.getActivation("quality"));
85
+ assert.deepEqual(quality?.objective, { kind: "highest-quality" });
86
+ assert.equal(quality?.evidenceDigest, "quality-evidence-v2");
87
+ for (const path of [costProfile, costActivation, globalConfig, globalActivation]) {
88
+ assert.equal(readFileSync(path, "utf8"), before.get(path), `${path} changed`);
89
+ }
90
+ const cost = await runRouteKitEffect(reader.getActivation("cost"));
91
+ assert.deepEqual(cost?.objective, {
92
+ kind: "lowest-cost",
93
+ minimumQuality: 0.7
94
+ });
95
+ assert.equal(cost?.evidenceDigest, "cost-evidence-v2");
96
+ for (const path of [qualityProfile, qualityActivation, globalConfig, globalActivation]) {
97
+ assert.equal(readFileSync(path, "utf8"), before.get(path), `${path} changed`);
98
+ }
99
+ for (const path of paths) {
100
+ assert.equal(readFileSync(path, "utf8"), before.get(path), `${path} changed`);
101
+ }
102
+ await assert.rejects(runRouteKitEffect(reader.getActivation()), /require an explicit profile selector/);
103
+ }
104
+ finally {
105
+ rmSync(root, { recursive: true, force: true });
106
+ }
107
+ });
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@velum-labs/routekit-daemon",
3
3
  "private": false,
4
- "version": "1.2.0",
4
+ "version": "1.3.0",
5
5
  "description": "Singleton RouteKit control daemon and stable model gateway.",
6
6
  "repository": {
7
7
  "type": "git",
@@ -44,15 +44,15 @@
44
44
  "effect": "4.0.0-rc.108",
45
45
  "posthog-node": "5.46.1",
46
46
  "yaml": "2.9.0",
47
- "@velum-labs/routekit-accounts": "1.2.0",
48
- "@velum-labs/routekit-config": "1.2.0",
49
- "@velum-labs/routekit-control": "1.2.0",
50
- "@velum-labs/routekit-eval-contracts": "1.2.0",
51
- "@velum-labs/routekit-eval-store": "1.2.0",
52
- "@velum-labs/routekit-gateway": "1.2.0",
53
- "@velum-labs/routekit-registry": "1.2.0",
54
- "@velum-labs/routekit-runtime": "1.2.0",
55
- "@velum-labs/routekit-telemetry-core": "1.2.0"
47
+ "@velum-labs/routekit-accounts": "1.3.0",
48
+ "@velum-labs/routekit-config": "1.3.0",
49
+ "@velum-labs/routekit-control": "1.3.0",
50
+ "@velum-labs/routekit-eval-contracts": "1.3.0",
51
+ "@velum-labs/routekit-eval-store": "1.3.0",
52
+ "@velum-labs/routekit-gateway": "1.3.0",
53
+ "@velum-labs/routekit-registry": "1.3.0",
54
+ "@velum-labs/routekit-runtime": "1.3.0",
55
+ "@velum-labs/routekit-telemetry-core": "1.3.0"
56
56
  },
57
57
  "scripts": {
58
58
  "build": "tsc -b",