@velum-labs/routekit-daemon 0.11.0 → 0.13.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, encodeJoinCredential, 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,63 @@ 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
+ ...(issued.plane === "control"
1264
+ ? {
1265
+ joinToken: encodeJoinCredential({
1266
+ publicRecordPath: daemonPublicRecordPath(home),
1267
+ token: issued.token
1268
+ })
1269
+ }
1270
+ : {})
1271
+ };
1272
+ }
1273
+ catch (error) {
1274
+ throw new ControlError({
1275
+ code: "bad_request",
1276
+ message: error instanceof Error ? error.message : String(error)
1277
+ });
1278
+ }
1279
+ },
1280
+ "tokens.list": async (params) => ({
1281
+ tokens: tokens.list(params.plane)
1282
+ }),
1283
+ "tokens.revoke": async (params) => {
1284
+ try {
1285
+ const revoked = tokens.revoke(params.id);
1286
+ if (revoked.plane === "data")
1287
+ dataTokenCache.delete(revoked.label);
1288
+ return revoked;
1289
+ }
1290
+ catch (error) {
1291
+ const message = error instanceof Error ? error.message : String(error);
1292
+ throw new ControlError({
1293
+ code: message.startsWith("unknown token") ? "not_found" : "bad_request",
1294
+ message
1295
+ });
1296
+ }
1187
1297
  }
1188
1298
  };
1189
1299
  control = await startControlServer({
@@ -1192,6 +1302,16 @@ export async function startRouteKitDaemon(options) {
1192
1302
  product: ROUTEKIT_PRODUCT,
1193
1303
  packageVersion: options.packageVersion,
1194
1304
  capabilities: [ROUTEKIT_CONTROL_CAPABILITY],
1305
+ authorize: (presented) => {
1306
+ const principal = tokens.resolve(presented, "control");
1307
+ if (principal === undefined)
1308
+ return undefined;
1309
+ return {
1310
+ id: principal.id,
1311
+ label: principal.label,
1312
+ role: principal.role
1313
+ };
1314
+ },
1195
1315
  onError: (error, context) => {
1196
1316
  const operation = context.method ?? "control transport";
1197
1317
  console.error(`RouteKit ${operation} failed (request ${context.requestId}):`, error);
@@ -1221,6 +1341,17 @@ export async function startRouteKitDaemon(options) {
1221
1341
  args: redactedProcessArgs(process.argv.slice(2)),
1222
1342
  cwd: process.cwd()
1223
1343
  });
1344
+ writeDaemonPublicRecord(home, {
1345
+ product: ROUTEKIT_PRODUCT,
1346
+ kind: ROUTEKIT_DAEMON_KIND,
1347
+ url: control.url,
1348
+ port: control.port,
1349
+ generation,
1350
+ protocolVersion: CONTROL_PROTOCOL_VERSION,
1351
+ dataUrl,
1352
+ dataPort: proxy.port(),
1353
+ startedAt
1354
+ });
1224
1355
  extendCleanupGrace(drainGraceMs + 10_000);
1225
1356
  let closeRun;
1226
1357
  const close = () => {
@@ -1238,6 +1369,7 @@ export async function startRouteKitDaemon(options) {
1238
1369
  if (portless?.enabled)
1239
1370
  portless.unregister("gateway");
1240
1371
  store.remove(ROUTEKIT_DAEMON_KIND, { ifPid: process.pid });
1372
+ removeDaemonPublicRecord(home);
1241
1373
  authority.release();
1242
1374
  lifecycle = "closed";
1243
1375
  })();
@@ -1274,6 +1406,7 @@ export async function startRouteKitDaemon(options) {
1274
1406
  portless.unregister("gateway");
1275
1407
  if (record !== undefined)
1276
1408
  store.remove(ROUTEKIT_DAEMON_KIND, { ifPid: process.pid });
1409
+ removeDaemonPublicRecord(home);
1277
1410
  authority.release();
1278
1411
  throw error;
1279
1412
  }
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.13.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-control": "0.13.0",
38
+ "@velum-labs/routekit-gateway": "0.13.0",
39
+ "@velum-labs/routekit-registry": "0.13.0",
40
+ "@velum-labs/routekit-router": "0.13.0",
41
+ "@velum-labs/routekit-config": "0.13.0",
42
+ "@velum-labs/routekit-accounts": "0.13.0",
43
+ "@velum-labs/routekit-telemetry-core": "0.13.0",
44
+ "@velum-labs/routekit-runtime": "0.13.0"
45
45
  },
46
46
  "scripts": {
47
47
  "build": "tsc -b",