@velum-labs/routekit-daemon 0.11.0 → 0.12.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.
@@ -31,6 +31,9 @@ export function callInspection(modelCall) {
31
31
  }
32
32
  const account = record(attribution?.account);
33
33
  const accountSeat = string(account?.seat);
34
+ const principal = record(attribution?.principal);
35
+ const principalTokenId = string(principal?.token_id);
36
+ const principalLabel = string(principal?.label);
34
37
  const nativeModel = string(attribution?.native_model);
35
38
  const estimateUsd = number(metadata?.cost_estimate_usd);
36
39
  const attempts = number(attribution?.attempts) ?? 1;
@@ -44,6 +47,14 @@ export function callInspection(modelCall) {
44
47
  provider,
45
48
  billingMode,
46
49
  ...(accountSeat !== undefined ? { account: { seat: accountSeat } } : {}),
50
+ ...(principalTokenId !== undefined
51
+ ? {
52
+ principal: {
53
+ tokenId: principalTokenId,
54
+ ...(principalLabel !== undefined ? { label: principalLabel } : {})
55
+ }
56
+ }
57
+ : {}),
47
58
  retries: {
48
59
  attempts,
49
60
  total: retries,
package/dist/index.d.ts CHANGED
@@ -23,4 +23,15 @@ export type RunningRouteKitDaemon = {
23
23
  close(): Promise<void>;
24
24
  reload(): Promise<void>;
25
25
  };
26
+ export type DaemonPublicRecord = {
27
+ product: string;
28
+ kind: string;
29
+ url: string;
30
+ port: number;
31
+ generation: number;
32
+ protocolVersion: string;
33
+ dataUrl?: string;
34
+ dataPort?: number;
35
+ startedAt: string;
36
+ };
26
37
  export declare function startRouteKitDaemon(options: RouteKitDaemonOptions): Promise<RunningRouteKitDaemon>;
package/dist/index.js CHANGED
@@ -14,7 +14,7 @@ import { createRouteKitControlHandler, ROUTEKIT_CONTROL_CAPABILITY } from "@velu
14
14
  import { startSwitchingGatewayProxy } from "@velum-labs/routekit-gateway";
15
15
  import { PROVIDERS, accountKindForCliproxyAuthType, resolveAccountConnector } from "@velum-labs/routekit-registry";
16
16
  import { startRouter } from "@velum-labs/routekit-router";
17
- import { acquireLifecycleLock, CONTROL_PROTOCOL_VERSION, ControlClient, ControlError, createPortlessSession, createServiceRecordStore, extendCleanupGrace, generateControlToken, nextServiceGeneration, processIdentity, registerCleanup, startControlServer, supervisorFromEnv, writeFileAtomic } from "@velum-labs/routekit-runtime";
17
+ import { acquireLifecycleLock, CONTROL_PROTOCOL_VERSION, ControlClient, ControlError, createPortlessSession, createServiceRecordStore, createTokenStore, extendCleanupGrace, generateControlToken, nextServiceGeneration, processIdentity, registerCleanup, SERVICE_HOME_MODE, startControlServer, supervisorFromEnv, writeFileAtomic } from "@velum-labs/routekit-runtime";
18
18
  import { createConsentManager } from "@velum-labs/routekit-telemetry-core";
19
19
  import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
20
20
  import { createCliproxySidecar } from "./cliproxy-sidecar.js";
@@ -42,16 +42,59 @@ function redactedProcessArgs(args) {
42
42
  }
43
43
  return result;
44
44
  }
45
- function resolveDataToken(home, input) {
45
+ function resolveDataToken(home, input, tokens) {
46
46
  const path = input.authTokenFile ?? dataTokenPath(home);
47
- mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
48
- const token = input.authToken ??
49
- (existsSync(path) ? readFileSync(path, "utf8").trim() : generateControlToken());
50
- if (token.length === 0)
47
+ const ensured = tokens.ensureOwnerDataToken({
48
+ ...(input.authToken !== undefined ? { plaintext: input.authToken } : {}),
49
+ legacyPath: path
50
+ });
51
+ if (ensured.token.length === 0)
51
52
  throw new Error("RouteKit data-plane token is empty");
52
- writeFileAtomic(path, `${token}\n`, { mode: 0o600 });
53
- chmodSync(path, 0o600);
54
- return { token, path };
53
+ return { token: ensured.token, path };
54
+ }
55
+ function daemonPublicRecordPath(home) {
56
+ return join(home, "services", "daemon.public.json");
57
+ }
58
+ /** Publish a secret-free discovery file peers can read across OS accounts. */
59
+ function writeDaemonPublicRecord(home, record) {
60
+ const servicesDir = join(home, "services");
61
+ mkdirSync(home, { recursive: true, mode: SERVICE_HOME_MODE });
62
+ chmodSync(home, SERVICE_HOME_MODE);
63
+ mkdirSync(servicesDir, { recursive: true, mode: SERVICE_HOME_MODE });
64
+ chmodSync(servicesDir, SERVICE_HOME_MODE);
65
+ const path = daemonPublicRecordPath(home);
66
+ writeFileAtomic(path, `${JSON.stringify(record, null, 2)}\n`, { mode: 0o644 });
67
+ chmodSync(path, 0o644);
68
+ }
69
+ function removeDaemonPublicRecord(home) {
70
+ rmSync(daemonPublicRecordPath(home), { force: true });
71
+ }
72
+ /**
73
+ * Resolve (or mint) a data-plane token for the calling control principal so
74
+ * tool launchers attribute usage to that admin rather than the owner token.
75
+ * Plaintext is cached in-memory for the daemon lifetime.
76
+ */
77
+ function dataTokenForPrincipal(tokens, cache, ownerToken, principal) {
78
+ if (principal === undefined || principal.role === "ephemeral" || principal.role === "owner") {
79
+ return ownerToken;
80
+ }
81
+ const label = `${principal.label}-data`;
82
+ const cached = cache.get(label);
83
+ if (cached !== undefined)
84
+ return cached;
85
+ const existing = tokens.findByLabel(label, "data");
86
+ if (existing !== undefined) {
87
+ // Registry has a hash but plaintext is gone after restart; rotate.
88
+ tokens.revoke(existing.id);
89
+ }
90
+ const issued = tokens.issue({
91
+ label,
92
+ plane: "data",
93
+ role: "admin",
94
+ createdBy: principal.label
95
+ });
96
+ cache.set(label, issued.token);
97
+ return issued.token;
55
98
  }
56
99
  function revisionPath(home) {
57
100
  return join(home, "daemon-revisions.json");
@@ -200,7 +243,10 @@ export async function startRouteKitDaemon(options) {
200
243
  const home = options.stateHome ?? routekitHome(env);
201
244
  const configPath = options.configPath ?? globalRouterConfigPath();
202
245
  const drainGraceMs = options.drainGraceMs ?? 30_000;
203
- const dataAuth = resolveDataToken(home, options);
246
+ const tokens = createTokenStore(home);
247
+ const dataTokenCache = new Map();
248
+ const dataAuth = resolveDataToken(home, options, tokens);
249
+ dataTokenCache.set("default", dataAuth.token);
204
250
  const store = createServiceRecordStore({ home, product: ROUTEKIT_PRODUCT });
205
251
  // Held for the daemon's whole lifetime. Lifecycle clients use daemon.lock
206
252
  // while this authority lock prevents any second daemon from becoming live.
@@ -302,7 +348,17 @@ export async function startRouteKitDaemon(options) {
302
348
  target: activeRouter.url,
303
349
  host: options.host ?? "127.0.0.1",
304
350
  port: options.port ?? 8080,
305
- authToken: dataAuth.token
351
+ authToken: dataAuth.token,
352
+ resolveDataPrincipal: (presented) => {
353
+ const principal = tokens.resolve(presented, "data");
354
+ if (principal === undefined)
355
+ return undefined;
356
+ return {
357
+ id: principal.id,
358
+ label: principal.label,
359
+ role: principal.role
360
+ };
361
+ }
306
362
  });
307
363
  portless = await createPortlessSession(options.portless ?? env.ROUTEKIT_PORTLESS !== "0", { project: "routekit", ownerLabel: "routekit-daemon", bareNames: [] });
308
364
  const dataUrl = portless.enabled
@@ -1165,7 +1221,7 @@ export async function startRouteKitDaemon(options) {
1165
1221
  ]
1166
1222
  };
1167
1223
  },
1168
- "launcher.prepare": async (params) => {
1224
+ "launcher.prepare": async (params, context) => {
1169
1225
  const listed = await handlers["models.list"]({}, {
1170
1226
  signal: new AbortController().signal,
1171
1227
  requestId: "internal"
@@ -1181,9 +1237,55 @@ export async function startRouteKitDaemon(options) {
1181
1237
  tool: params.tool,
1182
1238
  model,
1183
1239
  gatewayUrl: dataUrl,
1184
- authToken: dataAuth.token,
1240
+ authToken: dataTokenForPrincipal(tokens, dataTokenCache, dataAuth.token, context.principal),
1185
1241
  env: {}
1186
1242
  };
1243
+ },
1244
+ "tokens.issue": async (params, context) => {
1245
+ try {
1246
+ const issued = tokens.issue({
1247
+ label: params.label,
1248
+ plane: params.plane,
1249
+ role: "admin",
1250
+ createdBy: params.createdBy ??
1251
+ context.principal?.label ??
1252
+ "control"
1253
+ });
1254
+ if (issued.plane === "data") {
1255
+ dataTokenCache.set(issued.label, issued.token);
1256
+ }
1257
+ return {
1258
+ id: issued.id,
1259
+ label: issued.label,
1260
+ plane: issued.plane,
1261
+ role: issued.role,
1262
+ token: issued.token
1263
+ };
1264
+ }
1265
+ catch (error) {
1266
+ throw new ControlError({
1267
+ code: "bad_request",
1268
+ message: error instanceof Error ? error.message : String(error)
1269
+ });
1270
+ }
1271
+ },
1272
+ "tokens.list": async (params) => ({
1273
+ tokens: tokens.list(params.plane)
1274
+ }),
1275
+ "tokens.revoke": async (params) => {
1276
+ try {
1277
+ const revoked = tokens.revoke(params.id);
1278
+ if (revoked.plane === "data")
1279
+ dataTokenCache.delete(revoked.label);
1280
+ return revoked;
1281
+ }
1282
+ catch (error) {
1283
+ const message = error instanceof Error ? error.message : String(error);
1284
+ throw new ControlError({
1285
+ code: message.startsWith("unknown token") ? "not_found" : "bad_request",
1286
+ message
1287
+ });
1288
+ }
1187
1289
  }
1188
1290
  };
1189
1291
  control = await startControlServer({
@@ -1192,6 +1294,16 @@ export async function startRouteKitDaemon(options) {
1192
1294
  product: ROUTEKIT_PRODUCT,
1193
1295
  packageVersion: options.packageVersion,
1194
1296
  capabilities: [ROUTEKIT_CONTROL_CAPABILITY],
1297
+ authorize: (presented) => {
1298
+ const principal = tokens.resolve(presented, "control");
1299
+ if (principal === undefined)
1300
+ return undefined;
1301
+ return {
1302
+ id: principal.id,
1303
+ label: principal.label,
1304
+ role: principal.role
1305
+ };
1306
+ },
1195
1307
  onError: (error, context) => {
1196
1308
  const operation = context.method ?? "control transport";
1197
1309
  console.error(`RouteKit ${operation} failed (request ${context.requestId}):`, error);
@@ -1221,6 +1333,17 @@ export async function startRouteKitDaemon(options) {
1221
1333
  args: redactedProcessArgs(process.argv.slice(2)),
1222
1334
  cwd: process.cwd()
1223
1335
  });
1336
+ writeDaemonPublicRecord(home, {
1337
+ product: ROUTEKIT_PRODUCT,
1338
+ kind: ROUTEKIT_DAEMON_KIND,
1339
+ url: control.url,
1340
+ port: control.port,
1341
+ generation,
1342
+ protocolVersion: CONTROL_PROTOCOL_VERSION,
1343
+ dataUrl,
1344
+ dataPort: proxy.port(),
1345
+ startedAt
1346
+ });
1224
1347
  extendCleanupGrace(drainGraceMs + 10_000);
1225
1348
  let closeRun;
1226
1349
  const close = () => {
@@ -1238,6 +1361,7 @@ export async function startRouteKitDaemon(options) {
1238
1361
  if (portless?.enabled)
1239
1362
  portless.unregister("gateway");
1240
1363
  store.remove(ROUTEKIT_DAEMON_KIND, { ifPid: process.pid });
1364
+ removeDaemonPublicRecord(home);
1241
1365
  authority.release();
1242
1366
  lifecycle = "closed";
1243
1367
  })();
@@ -1274,6 +1398,7 @@ export async function startRouteKitDaemon(options) {
1274
1398
  portless.unregister("gateway");
1275
1399
  if (record !== undefined)
1276
1400
  store.remove(ROUTEKIT_DAEMON_KIND, { ifPid: process.pid });
1401
+ removeDaemonPublicRecord(home);
1277
1402
  authority.release();
1278
1403
  throw error;
1279
1404
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@velum-labs/routekit-daemon",
3
3
  "private": false,
4
- "version": "0.11.0",
4
+ "version": "0.12.0",
5
5
  "description": "Singleton RouteKit control daemon and stable model gateway.",
6
6
  "repository": {
7
7
  "type": "git",
@@ -34,14 +34,14 @@
34
34
  ],
35
35
  "dependencies": {
36
36
  "yaml": "2.9.0",
37
- "@velum-labs/routekit-config": "0.11.0",
38
- "@velum-labs/routekit-gateway": "0.11.0",
39
- "@velum-labs/routekit-control": "0.11.0",
40
- "@velum-labs/routekit-accounts": "0.11.0",
41
- "@velum-labs/routekit-router": "0.11.0",
42
- "@velum-labs/routekit-registry": "0.11.0",
43
- "@velum-labs/routekit-runtime": "0.11.0",
44
- "@velum-labs/routekit-telemetry-core": "0.11.0"
37
+ "@velum-labs/routekit-accounts": "0.12.0",
38
+ "@velum-labs/routekit-control": "0.12.0",
39
+ "@velum-labs/routekit-config": "0.12.0",
40
+ "@velum-labs/routekit-registry": "0.12.0",
41
+ "@velum-labs/routekit-gateway": "0.12.0",
42
+ "@velum-labs/routekit-router": "0.12.0",
43
+ "@velum-labs/routekit-runtime": "0.12.0",
44
+ "@velum-labs/routekit-telemetry-core": "0.12.0"
45
45
  },
46
46
  "scripts": {
47
47
  "build": "tsc -b",