@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 +8 -0
- package/dist/effect/daemon-live.js +17 -9
- package/dist/effect-api.d.ts +1 -0
- package/dist/effect-api.js +1 -0
- package/dist/eval-routing-policy.d.ts +1 -1
- package/dist/eval-routing-policy.js +89 -9
- package/dist/eval-routing-service.js +4 -2
- package/dist/launcher-service.js +59 -4
- package/dist/services/launcher-session/service.d.ts +23 -0
- package/dist/services/launcher-session/service.js +48 -0
- package/dist/test/application-services.test.js +24 -0
- package/dist/test/daemon-data-plane.test.js +114 -9
- package/dist/test/daemon-fixtures.js +9 -0
- package/dist/test/eval-routing-application-service.test.js +13 -2
- package/dist/test/eval-routing-policy.test.js +54 -2
- package/package.json +10 -10
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", {
|
|
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, {
|
|
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,
|
package/dist/effect-api.d.ts
CHANGED
|
@@ -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";
|
package/dist/effect-api.js
CHANGED
|
@@ -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: (
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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*
|
|
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,
|
package/dist/launcher-service.js
CHANGED
|
@@ -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
|
-
|
|
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 ||
|
|
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
|
-
|
|
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.
|
|
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.
|
|
48
|
-
"@velum-labs/routekit-config": "1.
|
|
49
|
-
"@velum-labs/routekit-control": "1.
|
|
50
|
-
"@velum-labs/routekit-eval-contracts": "1.
|
|
51
|
-
"@velum-labs/routekit-eval-store": "1.
|
|
52
|
-
"@velum-labs/routekit-gateway": "1.
|
|
53
|
-
"@velum-labs/routekit-registry": "1.
|
|
54
|
-
"@velum-labs/routekit-runtime": "1.
|
|
55
|
-
"@velum-labs/routekit-telemetry-core": "1.
|
|
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",
|