@onklave/agent-cli 0.1.44 → 0.1.46

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.
Files changed (2) hide show
  1. package/main.js +390 -192
  2. package/package.json +1 -1
package/main.js CHANGED
@@ -285,6 +285,20 @@ async function loginCommand(args) {
285
285
  process.exitCode = 1;
286
286
  return;
287
287
  }
288
+ const linkingTokenPrefixes = ["ok_link_", "ok_worker_", "ok_device_"];
289
+ if (linkingTokenPrefixes.some((prefix) => token.startsWith(prefix))) {
290
+ console.error(
291
+ "That looks like a worker/device linking token, not a login credential.\n"
292
+ );
293
+ console.error(
294
+ "Linking tokens register a machine \u2014 use: onklave register --token <token>"
295
+ );
296
+ console.error(
297
+ "To log in, generate a CLI token in the Portal under Settings > API Tokens."
298
+ );
299
+ process.exitCode = 1;
300
+ return;
301
+ }
288
302
  const metadata = {};
289
303
  if (typeof platformUrl === "string") {
290
304
  metadata.platformUrl = platformUrl;
@@ -729,6 +743,19 @@ var PlatformClient = class {
729
743
  async listMachines() {
730
744
  return this.request("GET", "/api/v1/runner/machines");
731
745
  }
746
+ /*
747
+ * Worker Node Security 1b — fetch this runner's server-side policy
748
+ * (org-admin-defined allowed work paths). Device-token authed; the endpoint
749
+ * is public and reads x-device-token.
750
+ */
751
+ async getRunnerPolicy(deviceToken) {
752
+ return this.request(
753
+ "GET",
754
+ "/api/v1/runner/policy",
755
+ void 0,
756
+ { "x-device-token": deviceToken }
757
+ );
758
+ }
732
759
  /*
733
760
  * Check platform health.
734
761
  */
@@ -747,8 +774,8 @@ var PlatformClient = class {
747
774
  /*
748
775
  * Make an authenticated HTTP request to the platform.
749
776
  */
750
- async request(method, path10, body, extraHeaders) {
751
- const url = `${this.baseUrl}${GATEWAY_SERVICE_PREFIX}${path10}`;
777
+ async request(method, path11, body, extraHeaders) {
778
+ const url = `${this.baseUrl}${GATEWAY_SERVICE_PREFIX}${path11}`;
752
779
  const headers = {
753
780
  Authorization: `Bearer ${this.token}`,
754
781
  "Content-Type": "application/json",
@@ -803,7 +830,7 @@ var CommsClient = class {
803
830
  * (e.g. `assignment:claim-available`) to this specific runner.
804
831
  */
805
832
  async connect(commsUrl, token, extra) {
806
- return new Promise((resolve3, reject) => {
833
+ return new Promise((resolve4, reject) => {
807
834
  const baseUrl = commsUrl.replace(/\/+$/, "");
808
835
  this.socket = io(`${baseUrl}/agent-cli`, {
809
836
  auth: {
@@ -828,7 +855,7 @@ var CommsClient = class {
828
855
  this.socket.on("connect", () => {
829
856
  clearTimeout(connectTimeout);
830
857
  this.reconnectAttempts = 0;
831
- resolve3();
858
+ resolve4();
832
859
  });
833
860
  this.socket.on("connect_error", (error) => {
834
861
  this.reconnectAttempts++;
@@ -914,6 +941,15 @@ var CommsClient = class {
914
941
  onAssignmentAvailable(handler) {
915
942
  this.socket?.on("assignment:claim-available", handler);
916
943
  }
944
+ /*
945
+ * Listen for daemon:upgrade requests pushed by an org admin from the
946
+ * portal. The daemon only acts on these when auto-upgrade is opted in
947
+ * (ONKLAVE_ALLOW_AUTO_UPGRADE) — see DaemonUpgradeService. Worker Node
948
+ * Security Phase 3.
949
+ */
950
+ onUpgradeRequest(handler) {
951
+ this.socket?.on("daemon:upgrade", handler);
952
+ }
917
953
  };
918
954
 
919
955
  // _apps/@onklave/agent-cli/src/services/session-manager.ts
@@ -1004,16 +1040,16 @@ var SessionManager = class {
1004
1040
  throw new Error(`No active session found with ID: ${sessionId}`);
1005
1041
  }
1006
1042
  session.process.kill("SIGTERM");
1007
- await new Promise((resolve3) => {
1043
+ await new Promise((resolve4) => {
1008
1044
  const forceTimeout = setTimeout(() => {
1009
1045
  if (session.process.killed === false) {
1010
1046
  session.process.kill("SIGKILL");
1011
1047
  }
1012
- resolve3();
1048
+ resolve4();
1013
1049
  }, 5e3);
1014
1050
  session.process.on("exit", () => {
1015
1051
  clearTimeout(forceTimeout);
1016
- resolve3();
1052
+ resolve4();
1017
1053
  });
1018
1054
  });
1019
1055
  this.activeSessions.delete(sessionId);
@@ -1213,7 +1249,50 @@ var AuditStreamer = class {
1213
1249
  };
1214
1250
 
1215
1251
  // _apps/@onklave/agent-cli/src/services/guardrail-enforcer.ts
1252
+ import * as fs3 from "fs";
1216
1253
  import * as path3 from "path";
1254
+ function safeRealpath(target) {
1255
+ let current = path3.resolve(target);
1256
+ const tail = [];
1257
+ for (let i = 0; i < 4096; i++) {
1258
+ try {
1259
+ const real = fs3.realpathSync(current);
1260
+ return tail.length ? path3.join(real, ...tail.reverse()) : real;
1261
+ } catch {
1262
+ const parent = path3.dirname(current);
1263
+ if (parent === current) break;
1264
+ tail.push(path3.basename(current));
1265
+ current = parent;
1266
+ }
1267
+ }
1268
+ return path3.resolve(target);
1269
+ }
1270
+ function isPathWithin(target, root) {
1271
+ const realRoot = safeRealpath(root);
1272
+ const realTarget = safeRealpath(target);
1273
+ if (realTarget === realRoot) return true;
1274
+ const rootWithSep = realRoot.endsWith(path3.sep) ? realRoot : realRoot + path3.sep;
1275
+ return realTarget.startsWith(rootWithSep);
1276
+ }
1277
+ function checkWorkspaceAccess(contextPath, allowedRoots) {
1278
+ const real = safeRealpath(contextPath);
1279
+ if (real === path3.parse(real).root) {
1280
+ return {
1281
+ allowed: false,
1282
+ reason: `Refusing to run with the filesystem root "${real}" as the workspace.`
1283
+ };
1284
+ }
1285
+ if (allowedRoots.length > 0) {
1286
+ const within = allowedRoots.some((root) => isPathWithin(contextPath, root));
1287
+ if (!within) {
1288
+ return {
1289
+ allowed: false,
1290
+ reason: `Working directory "${contextPath}" is outside the allowed paths.`
1291
+ };
1292
+ }
1293
+ }
1294
+ return { allowed: true };
1295
+ }
1217
1296
  var GuardrailEnforcer = class {
1218
1297
  constructor(config) {
1219
1298
  this.config = config;
@@ -1272,11 +1351,11 @@ var GuardrailEnforcer = class {
1272
1351
  * Check if a file path is accessible for the given operation.
1273
1352
  */
1274
1353
  checkPathAccess(filePath, operation) {
1275
- const normalizedPath = path3.resolve(filePath);
1354
+ const realPath = safeRealpath(filePath);
1276
1355
  if (operation === "write" || operation === "delete") {
1277
1356
  if (this.config.writablePaths.length > 0) {
1278
1357
  const isWritable = this.config.writablePaths.some(
1279
- (wp) => normalizedPath.startsWith(path3.resolve(wp))
1358
+ (wp) => isPathWithin(filePath, wp)
1280
1359
  );
1281
1360
  if (!isWritable) {
1282
1361
  return {
@@ -1289,7 +1368,7 @@ var GuardrailEnforcer = class {
1289
1368
  if (operation === "read") {
1290
1369
  if (this.config.readablePaths.length > 0) {
1291
1370
  const isReadable = this.config.readablePaths.some(
1292
- (rp) => normalizedPath.startsWith(path3.resolve(rp))
1371
+ (rp) => isPathWithin(filePath, rp)
1293
1372
  );
1294
1373
  if (!isReadable) {
1295
1374
  return {
@@ -1308,9 +1387,10 @@ var GuardrailEnforcer = class {
1308
1387
  "id_rsa",
1309
1388
  "id_ed25519"
1310
1389
  ];
1311
- const basename3 = path3.basename(normalizedPath);
1390
+ const basename3 = path3.basename(realPath);
1391
+ const normForward = realPath.split(path3.sep).join("/");
1312
1392
  for (const sensitive of sensitivePatterns) {
1313
- if (basename3 === sensitive || normalizedPath.includes(`/${sensitive}`)) {
1393
+ if (basename3 === sensitive || normForward.includes(`/${sensitive}`)) {
1314
1394
  return {
1315
1395
  allowed: false,
1316
1396
  reason: `Access to sensitive path "${filePath}" is blocked by default`
@@ -1417,7 +1497,9 @@ var HeartbeatService = class {
1417
1497
  machineId: this.opts.machineId,
1418
1498
  status: "online",
1419
1499
  activeSessionCount: this.opts.getActiveSessionCount(),
1420
- resourceUsage: this.opts.getResourceUsage?.()
1500
+ resourceUsage: this.opts.getResourceUsage?.(),
1501
+ cliVersion: this.opts.cliVersion,
1502
+ posture: this.opts.posture
1421
1503
  };
1422
1504
  try {
1423
1505
  const response = await fetch(
@@ -1446,6 +1528,141 @@ var HeartbeatService = class {
1446
1528
  }
1447
1529
  };
1448
1530
 
1531
+ // _apps/@onklave/agent-cli/src/services/cli-version.ts
1532
+ import * as fs4 from "node:fs";
1533
+ import * as path4 from "node:path";
1534
+ import { fileURLToPath } from "node:url";
1535
+
1536
+ // _apps/@onklave/agent-cli/src/services/daemon-update-checker.service.ts
1537
+ var REGISTRY_URL = "https://registry.npmjs.org";
1538
+ var PACKAGE_NAME = "@onklave/agent-cli";
1539
+ var DaemonUpdateChecker = class {
1540
+ constructor(opts) {
1541
+ this.latestVersion = null;
1542
+ this.lastCheckedAt = null;
1543
+ this.emittedForVersion = null;
1544
+ this.currentVersion = opts.currentVersion;
1545
+ this.audit = opts.audit;
1546
+ this.registryFetcher = opts.registryFetcher ?? defaultRegistryFetcher;
1547
+ }
1548
+ /**
1549
+ * Hit the registry. On success, sets latestVersion and (if newer than
1550
+ * current) emits `daemon.update_available` — exactly once per version,
1551
+ * even across repeated checks. On failure, logs and returns; the next
1552
+ * tick can retry.
1553
+ */
1554
+ async check() {
1555
+ try {
1556
+ const latest = await this.registryFetcher(PACKAGE_NAME);
1557
+ this.latestVersion = latest;
1558
+ this.lastCheckedAt = /* @__PURE__ */ new Date();
1559
+ if (isNewerVersion(latest, this.currentVersion) && this.emittedForVersion !== latest) {
1560
+ this.audit.record({
1561
+ sessionId: this.currentVersion,
1562
+ type: "daemon_lifecycle" /* DAEMON_LIFECYCLE */,
1563
+ action: DAEMON_AUDIT_ACTIONS.UPDATE_AVAILABLE,
1564
+ details: {
1565
+ currentVersion: this.currentVersion,
1566
+ latestVersion: latest
1567
+ },
1568
+ outcome: "success"
1569
+ });
1570
+ this.emittedForVersion = latest;
1571
+ }
1572
+ } catch (err) {
1573
+ console.warn(
1574
+ `[daemon] update check failed: ${err.message} (will retry)`
1575
+ );
1576
+ }
1577
+ }
1578
+ info() {
1579
+ return {
1580
+ currentVersion: this.currentVersion,
1581
+ latestVersion: this.latestVersion,
1582
+ updateAvailable: !!this.latestVersion && isNewerVersion(this.latestVersion, this.currentVersion),
1583
+ checkedAt: this.lastCheckedAt?.toISOString() ?? null
1584
+ };
1585
+ }
1586
+ };
1587
+ async function defaultRegistryFetcher(packageName) {
1588
+ const url = `${REGISTRY_URL}/${encodeURIComponent(packageName)}/latest`;
1589
+ const response = await fetch(url, {
1590
+ method: "GET",
1591
+ headers: { Accept: "application/json" },
1592
+ signal: AbortSignal.timeout(1e4)
1593
+ });
1594
+ if (!response.ok) {
1595
+ throw new Error(`registry ${response.status} ${response.statusText}`);
1596
+ }
1597
+ const body = await response.json();
1598
+ if (!body.version) throw new Error("registry response missing version");
1599
+ return body.version;
1600
+ }
1601
+ function isNewerVersion(a, b) {
1602
+ const pa = parseSemverNumeric(a);
1603
+ const pb = parseSemverNumeric(b);
1604
+ if (!pa || !pb) return false;
1605
+ for (let i = 0; i < 3; i += 1) {
1606
+ if (pa[i] > pb[i]) return true;
1607
+ if (pa[i] < pb[i]) return false;
1608
+ }
1609
+ return false;
1610
+ }
1611
+ function parseSemverNumeric(v) {
1612
+ const match = /^(\d+)\.(\d+)\.(\d+)/.exec(v);
1613
+ if (!match) return null;
1614
+ return [
1615
+ Number.parseInt(match[1], 10),
1616
+ Number.parseInt(match[2], 10),
1617
+ Number.parseInt(match[3], 10)
1618
+ ];
1619
+ }
1620
+
1621
+ // _apps/@onklave/agent-cli/src/services/cli-version.ts
1622
+ function readPackageVersion() {
1623
+ try {
1624
+ const moduleDir = path4.dirname(fileURLToPath(import.meta.url));
1625
+ const candidates = [
1626
+ path4.join(moduleDir, "package.json"),
1627
+ // bundled: dist root
1628
+ path4.join(moduleDir, "..", "package.json"),
1629
+ path4.join(moduleDir, "..", "..", "package.json"),
1630
+ // dev: src/services -> root
1631
+ path4.join(moduleDir, "..", "..", "..", "package.json")
1632
+ ];
1633
+ for (const p of candidates) {
1634
+ if (!fs4.existsSync(p)) continue;
1635
+ const pkg = JSON.parse(fs4.readFileSync(p, "utf8"));
1636
+ if (pkg.name === PACKAGE_NAME && pkg.version) return pkg.version;
1637
+ }
1638
+ } catch {
1639
+ }
1640
+ return null;
1641
+ }
1642
+ function getCurrentVersion() {
1643
+ return readPackageVersion() ?? "0.0.0";
1644
+ }
1645
+
1646
+ // _apps/@onklave/agent-cli/src/services/posture.ts
1647
+ import * as fs5 from "fs";
1648
+ import * as os3 from "os";
1649
+ function collectPosture() {
1650
+ let dockerized = false;
1651
+ try {
1652
+ dockerized = fs5.existsSync("/.dockerenv") || !!process.env["ONKLAVE_RUNNER_DOCKERIZED"];
1653
+ } catch {
1654
+ }
1655
+ let runningAsRoot = false;
1656
+ try {
1657
+ runningAsRoot = typeof process.getuid === "function" && process.getuid() === 0;
1658
+ } catch {
1659
+ }
1660
+ return { osVersion: os3.release(), dockerized, runningAsRoot };
1661
+ }
1662
+
1663
+ // _apps/@onklave/agent-cli/src/commands/run.command.ts
1664
+ import * as path5 from "path";
1665
+
1449
1666
  // _apps/@onklave/agent-cli/src/tui/render.tsx
1450
1667
  import { render } from "ink";
1451
1668
 
@@ -2032,6 +2249,27 @@ async function runCommand(args) {
2032
2249
  resolvedConfig.platformUrl,
2033
2250
  creds.token
2034
2251
  );
2252
+ let serverAllowedPaths = [];
2253
+ if (creds.machineId && creds.deviceToken) {
2254
+ try {
2255
+ const policy = await platformClient.getRunnerPolicy(creds.deviceToken);
2256
+ serverAllowedPaths = policy.allowedWorkPaths ?? [];
2257
+ } catch (err) {
2258
+ console.warn(
2259
+ `Warning: could not fetch runner policy: ${err.message}`
2260
+ );
2261
+ }
2262
+ }
2263
+ const effectiveAllowedRoots = serverAllowedPaths.length ? serverAllowedPaths : [...resolvedConfig.writablePaths, ...resolvedConfig.readablePaths];
2264
+ const workspaceVerdict = checkWorkspaceAccess(
2265
+ resolvedConfig.context,
2266
+ effectiveAllowedRoots
2267
+ );
2268
+ if (!workspaceVerdict.allowed) {
2269
+ console.error(`Error: ${workspaceVerdict.reason}`);
2270
+ process.exitCode = 1;
2271
+ return;
2272
+ }
2035
2273
  const sessionManager = new SessionManager();
2036
2274
  let heartbeat = null;
2037
2275
  if (creds.machineId && creds.deviceToken) {
@@ -2039,7 +2277,9 @@ async function runCommand(args) {
2039
2277
  platformUrl: resolvedConfig.platformUrl,
2040
2278
  deviceToken: creds.deviceToken,
2041
2279
  machineId: creds.machineId,
2042
- getActiveSessionCount: () => sessionManager.getActiveSessionIds().length
2280
+ getActiveSessionCount: () => sessionManager.getActiveSessionIds().length,
2281
+ cliVersion: getCurrentVersion(),
2282
+ posture: collectPosture()
2043
2283
  });
2044
2284
  heartbeat.start();
2045
2285
  const stopHeartbeat = () => heartbeat?.stop();
@@ -2121,8 +2361,8 @@ async function runCommand(args) {
2121
2361
  }
2122
2362
  const addDirs = [
2123
2363
  .../* @__PURE__ */ new Set([
2124
- ...resolvedConfig.writablePaths,
2125
- ...resolvedConfig.readablePaths
2364
+ path5.resolve(resolvedConfig.context),
2365
+ ...effectiveAllowedRoots
2126
2366
  ])
2127
2367
  ];
2128
2368
  const sessionConfig = {
@@ -2235,15 +2475,15 @@ async function runCommand(args) {
2235
2475
  }
2236
2476
  }
2237
2477
  });
2238
- await new Promise((resolve3) => {
2478
+ await new Promise((resolve4) => {
2239
2479
  const checkInterval = setInterval(() => {
2240
2480
  if (!sessionManager.isSessionActive(sessionId)) {
2241
2481
  clearInterval(checkInterval);
2242
- resolve3();
2482
+ resolve4();
2243
2483
  }
2244
2484
  }, 500);
2245
2485
  });
2246
- await new Promise((resolve3) => setTimeout(resolve3, 1e3));
2486
+ await new Promise((resolve4) => setTimeout(resolve4, 1e3));
2247
2487
  tuiInstance.unmount();
2248
2488
  } catch (err) {
2249
2489
  tuiInstance.unmount();
@@ -2349,11 +2589,11 @@ Session timed out after ${resolvedConfig.timeout} seconds.`
2349
2589
  commsClient.disconnect();
2350
2590
  }
2351
2591
  });
2352
- await new Promise((resolve3) => {
2592
+ await new Promise((resolve4) => {
2353
2593
  const checkInterval = setInterval(() => {
2354
2594
  if (!sessionManager.isSessionActive(sessionId)) {
2355
2595
  clearInterval(checkInterval);
2356
- resolve3();
2596
+ resolve4();
2357
2597
  }
2358
2598
  }, 500);
2359
2599
  });
@@ -2584,8 +2824,8 @@ async function denyCommand(args) {
2584
2824
  }
2585
2825
 
2586
2826
  // _apps/@onklave/agent-cli/src/commands/doctor.command.ts
2587
- import * as fs3 from "fs";
2588
- import * as path4 from "path";
2827
+ import * as fs6 from "fs";
2828
+ import * as path6 from "path";
2589
2829
  import { execSync } from "child_process";
2590
2830
  async function doctorCommand() {
2591
2831
  console.log("Onklave Agent CLI \u2014 Doctor\n");
@@ -2693,10 +2933,10 @@ function checkNodeVersion() {
2693
2933
  };
2694
2934
  }
2695
2935
  function checkProjectConfig() {
2696
- const configPath = path4.join(process.cwd(), ".onklave.json");
2697
- if (fs3.existsSync(configPath)) {
2936
+ const configPath = path6.join(process.cwd(), ".onklave.json");
2937
+ if (fs6.existsSync(configPath)) {
2698
2938
  try {
2699
- const raw = fs3.readFileSync(configPath, "utf-8");
2939
+ const raw = fs6.readFileSync(configPath, "utf-8");
2700
2940
  JSON.parse(raw);
2701
2941
  return {
2702
2942
  name: "Project config",
@@ -2746,18 +2986,18 @@ async function checkWebSocket() {
2746
2986
  }
2747
2987
 
2748
2988
  // _apps/@onklave/agent-cli/src/commands/init.command.ts
2749
- import * as fs4 from "fs";
2750
- import * as path5 from "path";
2989
+ import * as fs7 from "fs";
2990
+ import * as path7 from "path";
2751
2991
  async function initCommand() {
2752
- const configPath = path5.join(process.cwd(), ".onklave.json");
2753
- if (fs4.existsSync(configPath)) {
2992
+ const configPath = path7.join(process.cwd(), ".onklave.json");
2993
+ if (fs7.existsSync(configPath)) {
2754
2994
  console.error("Error: .onklave.json already exists in this directory.");
2755
2995
  console.log("To overwrite, delete the existing file first.");
2756
2996
  process.exitCode = 1;
2757
2997
  return;
2758
2998
  }
2759
2999
  const template = {
2760
- project: path5.basename(process.cwd()),
3000
+ project: path7.basename(process.cwd()),
2761
3001
  org: "",
2762
3002
  description: "",
2763
3003
  defaults: {
@@ -2792,7 +3032,7 @@ async function initCommand() {
2792
3032
  system_prompt_append: ""
2793
3033
  }
2794
3034
  };
2795
- fs4.writeFileSync(
3035
+ fs7.writeFileSync(
2796
3036
  configPath,
2797
3037
  JSON.stringify(template, null, 2) + "\n",
2798
3038
  "utf-8"
@@ -2879,7 +3119,7 @@ async function configCommand(args) {
2879
3119
  }
2880
3120
 
2881
3121
  // _apps/@onklave/agent-cli/src/commands/register.command.ts
2882
- import * as os3 from "os";
3122
+ import * as os4 from "os";
2883
3123
  async function registerCommand(args) {
2884
3124
  const { flags } = parseArgs(args);
2885
3125
  const refresh = flags["refresh"] === true;
@@ -2891,8 +3131,8 @@ async function registerCommand(args) {
2891
3131
  console.log(" onklave register --refresh # re-detect capabilities");
2892
3132
  console.log("\nTo obtain a linking token:");
2893
3133
  console.log(" 1. Log in to the Onklave Portal");
2894
- console.log(" 2. Navigate to Settings > Machines");
2895
- console.log(' 3. Click "Register Machine" to generate a linking token');
3134
+ console.log(" 2. Navigate to Worker Nodes");
3135
+ console.log(' 3. Click "Add Worker Node" to generate a linking token');
2896
3136
  process.exitCode = 1;
2897
3137
  return;
2898
3138
  }
@@ -2926,11 +3166,13 @@ Machine ${creds.machineId} updated successfully.`);
2926
3166
  return;
2927
3167
  }
2928
3168
  const metadata = {
2929
- hostname: os3.hostname(),
2930
- os: `${os3.platform()} ${os3.release()}`,
2931
- arch: os3.arch(),
3169
+ hostname: os4.hostname(),
3170
+ os: `${os4.platform()} ${os4.release()}`,
3171
+ arch: os4.arch(),
2932
3172
  nodeVersion: process.version,
2933
- capabilities: detectCapabilities()
3173
+ cliVersion: getCurrentVersion(),
3174
+ capabilities: detectCapabilities(),
3175
+ posture: collectPosture()
2934
3176
  };
2935
3177
  try {
2936
3178
  console.log("Registering machine with Onklave platform...");
@@ -3021,7 +3263,7 @@ async function logsCommand(args) {
3021
3263
  }
3022
3264
 
3023
3265
  // _apps/@onklave/agent-cli/src/commands/daemon.command.ts
3024
- import * as fs8 from "fs";
3266
+ import * as fs10 from "fs";
3025
3267
 
3026
3268
  // _apps/@onklave/agent-cli/src/services/daemon-comms.service.ts
3027
3269
  var DaemonCommsService = class {
@@ -3095,7 +3337,7 @@ var DaemonCommsService = class {
3095
3337
  }
3096
3338
  };
3097
3339
  function defaultSleep(ms) {
3098
- return new Promise((resolve3) => setTimeout(resolve3, ms));
3340
+ return new Promise((resolve4) => setTimeout(resolve4, ms));
3099
3341
  }
3100
3342
 
3101
3343
  // _apps/@onklave/agent-cli/src/services/daemon-claim-handler.service.ts
@@ -3448,8 +3690,8 @@ var PlatformBrokerClient = class {
3448
3690
  params
3449
3691
  );
3450
3692
  }
3451
- async request(method, path10, body) {
3452
- const url = `${this.baseUrl}/agent-orchestration${path10}`;
3693
+ async request(method, path11, body) {
3694
+ const url = `${this.baseUrl}/agent-orchestration${path11}`;
3453
3695
  const response = await fetch(url, {
3454
3696
  method,
3455
3697
  headers: {
@@ -3485,9 +3727,9 @@ var PlatformBrokerClient = class {
3485
3727
 
3486
3728
  // _apps/@onklave/agent-cli/src/services/work-item-runner.service.ts
3487
3729
  import { spawn as spawn2 } from "child_process";
3488
- import * as fs5 from "fs";
3489
- import * as os4 from "os";
3490
- import * as path6 from "path";
3730
+ import * as fs8 from "fs";
3731
+ import * as os5 from "os";
3732
+ import * as path8 from "path";
3491
3733
  var WorkItemRunner = class {
3492
3734
  constructor(opts = {}) {
3493
3735
  this.sessionManager = opts.sessionManager ?? new SessionManager();
@@ -3508,10 +3750,10 @@ var WorkItemRunner = class {
3508
3750
  summary: `Work item ${workItem.id} has no clonable repo (project.repos[0].url missing).`
3509
3751
  };
3510
3752
  }
3511
- const workDir = fs5.mkdtempSync(
3512
- path6.join(os4.tmpdir(), `onklave-pickup-${sessionId}-`)
3753
+ const workDir = fs8.mkdtempSync(
3754
+ path8.join(os5.tmpdir(), `onklave-pickup-${sessionId}-`)
3513
3755
  );
3514
- const repoDir = path6.join(workDir, sanitizeName(repo.name) || "repo");
3756
+ const repoDir = path8.join(workDir, sanitizeName(repo.name) || "repo");
3515
3757
  const branchName = `onklave/work-item/${workItem.id}`;
3516
3758
  try {
3517
3759
  await this.git(["clone", repo.url, repoDir], workDir);
@@ -3563,7 +3805,7 @@ var WorkItemRunner = class {
3563
3805
  systemPromptAppend: null
3564
3806
  };
3565
3807
  let output = "";
3566
- const exitCode = await new Promise((resolve3) => {
3808
+ const exitCode = await new Promise((resolve4) => {
3567
3809
  const timeout = setTimeout(() => {
3568
3810
  void this.sessionManager.stopSession(sessionId).catch(() => void 0);
3569
3811
  }, this.timeoutSeconds * 1e3);
@@ -3576,7 +3818,7 @@ var WorkItemRunner = class {
3576
3818
  },
3577
3819
  onExit: (code) => {
3578
3820
  clearTimeout(timeout);
3579
- resolve3(code);
3821
+ resolve4(code);
3580
3822
  }
3581
3823
  });
3582
3824
  });
@@ -3609,12 +3851,12 @@ function sanitizeName(name) {
3609
3851
  }
3610
3852
  function cleanup(dir) {
3611
3853
  try {
3612
- fs5.rmSync(dir, { recursive: true, force: true });
3854
+ fs8.rmSync(dir, { recursive: true, force: true });
3613
3855
  } catch {
3614
3856
  }
3615
3857
  }
3616
3858
  function defaultGit(args, cwd) {
3617
- return new Promise((resolve3, reject) => {
3859
+ return new Promise((resolve4, reject) => {
3618
3860
  const child = spawn2("git", args, {
3619
3861
  cwd,
3620
3862
  stdio: ["ignore", "pipe", "pipe"]
@@ -3626,7 +3868,7 @@ function defaultGit(args, cwd) {
3626
3868
  child.on("error", (err) => reject(err));
3627
3869
  child.on("exit", (code) => {
3628
3870
  if (code === 0) {
3629
- resolve3();
3871
+ resolve4();
3630
3872
  } else {
3631
3873
  reject(new Error(`git ${args[0]} exited ${code}: ${stderr.trim()}`));
3632
3874
  }
@@ -3635,7 +3877,7 @@ function defaultGit(args, cwd) {
3635
3877
  }
3636
3878
 
3637
3879
  // _apps/@onklave/agent-cli/src/services/daemon-resource-sampler.service.ts
3638
- import * as os5 from "os";
3880
+ import * as os6 from "os";
3639
3881
  var DEFAULT_SAMPLE_INTERVAL_MS = 5e3;
3640
3882
  var DEFAULT_WINDOW_MS = 6e4;
3641
3883
  var DaemonResourceSampler = class {
@@ -3703,8 +3945,8 @@ var DaemonResourceSampler = class {
3703
3945
  }
3704
3946
  };
3705
3947
  function defaultCpuPercent() {
3706
- const cpus2 = os5.cpus().length || 1;
3707
- const load1m = os5.loadavg()[0];
3948
+ const cpus2 = os6.cpus().length || 1;
3949
+ const load1m = os6.loadavg()[0];
3708
3950
  return load1m / cpus2 * 100;
3709
3951
  }
3710
3952
  function defaultMemoryRss() {
@@ -3735,7 +3977,7 @@ var DaemonSpawner = class {
3735
3977
  orgId: req.orgId,
3736
3978
  systemPromptAppend: null
3737
3979
  };
3738
- return new Promise((resolve3, reject) => {
3980
+ return new Promise((resolve4, reject) => {
3739
3981
  void this.sessionManager.spawnSession(req.sessionId, config, {
3740
3982
  onStdout: (data) => {
3741
3983
  process.stdout.write(data);
@@ -3745,7 +3987,7 @@ var DaemonSpawner = class {
3745
3987
  },
3746
3988
  onExit: (code, signal) => {
3747
3989
  if (code === 0) {
3748
- resolve3();
3990
+ resolve4();
3749
3991
  } else {
3750
3992
  reject(
3751
3993
  new Error(
@@ -3896,123 +4138,48 @@ var DaemonTokenRefresher = class {
3896
4138
  }
3897
4139
  };
3898
4140
 
3899
- // _apps/@onklave/agent-cli/src/services/daemon-update-checker.service.ts
3900
- var REGISTRY_URL = "https://registry.npmjs.org";
3901
- var PACKAGE_NAME = "@onklave/agent-cli";
3902
- var DaemonUpdateChecker = class {
4141
+ // _apps/@onklave/agent-cli/src/services/daemon-upgrade.service.ts
4142
+ var DaemonUpgradeService = class {
3903
4143
  constructor(opts) {
3904
- this.latestVersion = null;
3905
- this.lastCheckedAt = null;
3906
- this.emittedForVersion = null;
3907
- this.currentVersion = opts.currentVersion;
3908
- this.audit = opts.audit;
3909
- this.registryFetcher = opts.registryFetcher ?? defaultRegistryFetcher;
4144
+ this.opts = opts;
3910
4145
  }
3911
- /**
3912
- * Hit the registry. On success, sets latestVersion and (if newer than
3913
- * current) emits `daemon.update_available` — exactly once per version,
3914
- * even across repeated checks. On failure, logs and returns; the next
3915
- * tick can retry.
3916
- */
3917
- async check() {
4146
+ async handle() {
4147
+ const { allowAutoUpgrade, currentVersion } = this.opts;
4148
+ if (!allowAutoUpgrade) {
4149
+ this.opts.audit("upgrade_requested_denied", {
4150
+ reason: "auto_upgrade_disabled",
4151
+ currentVersion
4152
+ });
4153
+ this.opts.log?.(
4154
+ "[daemon] upgrade requested, but auto-upgrade is disabled (set ONKLAVE_ALLOW_AUTO_UPGRADE=1 to opt in). Upgrade manually: npm install -g @onklave/agent-cli@latest"
4155
+ );
4156
+ return;
4157
+ }
4158
+ this.opts.audit("upgrade_started", { from: currentVersion });
3918
4159
  try {
3919
- const latest = await this.registryFetcher(PACKAGE_NAME);
3920
- this.latestVersion = latest;
3921
- this.lastCheckedAt = /* @__PURE__ */ new Date();
3922
- if (isNewerVersion(latest, this.currentVersion) && this.emittedForVersion !== latest) {
3923
- this.audit.record({
3924
- sessionId: this.currentVersion,
3925
- type: "daemon_lifecycle" /* DAEMON_LIFECYCLE */,
3926
- action: DAEMON_AUDIT_ACTIONS.UPDATE_AVAILABLE,
3927
- details: {
3928
- currentVersion: this.currentVersion,
3929
- latestVersion: latest
3930
- },
3931
- outcome: "success"
3932
- });
3933
- this.emittedForVersion = latest;
3934
- }
3935
- } catch (err) {
3936
- console.warn(
3937
- `[daemon] update check failed: ${err.message} (will retry)`
4160
+ const newVersion = await this.opts.runInstall();
4161
+ this.opts.audit("upgrade_completed", {
4162
+ from: currentVersion,
4163
+ to: newVersion
4164
+ });
4165
+ this.opts.log?.(
4166
+ `[daemon] upgraded ${currentVersion} -> ${newVersion}; restarting for supervisor pickup.`
3938
4167
  );
4168
+ this.opts.requestRestart();
4169
+ } catch (err) {
4170
+ this.opts.audit("upgrade_failed", {
4171
+ from: currentVersion,
4172
+ error: err.message
4173
+ });
4174
+ this.opts.log?.(`[daemon] upgrade failed: ${err.message}`);
3939
4175
  }
3940
4176
  }
3941
- info() {
3942
- return {
3943
- currentVersion: this.currentVersion,
3944
- latestVersion: this.latestVersion,
3945
- updateAvailable: !!this.latestVersion && isNewerVersion(this.latestVersion, this.currentVersion),
3946
- checkedAt: this.lastCheckedAt?.toISOString() ?? null
3947
- };
3948
- }
3949
4177
  };
3950
- async function defaultRegistryFetcher(packageName) {
3951
- const url = `${REGISTRY_URL}/${encodeURIComponent(packageName)}/latest`;
3952
- const response = await fetch(url, {
3953
- method: "GET",
3954
- headers: { Accept: "application/json" },
3955
- signal: AbortSignal.timeout(1e4)
3956
- });
3957
- if (!response.ok) {
3958
- throw new Error(`registry ${response.status} ${response.statusText}`);
3959
- }
3960
- const body = await response.json();
3961
- if (!body.version) throw new Error("registry response missing version");
3962
- return body.version;
3963
- }
3964
- function isNewerVersion(a, b) {
3965
- const pa = parseSemverNumeric(a);
3966
- const pb = parseSemverNumeric(b);
3967
- if (!pa || !pb) return false;
3968
- for (let i = 0; i < 3; i += 1) {
3969
- if (pa[i] > pb[i]) return true;
3970
- if (pa[i] < pb[i]) return false;
3971
- }
3972
- return false;
3973
- }
3974
- function parseSemverNumeric(v) {
3975
- const match = /^(\d+)\.(\d+)\.(\d+)/.exec(v);
3976
- if (!match) return null;
3977
- return [
3978
- Number.parseInt(match[1], 10),
3979
- Number.parseInt(match[2], 10),
3980
- Number.parseInt(match[3], 10)
3981
- ];
3982
- }
3983
-
3984
- // _apps/@onklave/agent-cli/src/services/cli-version.ts
3985
- import * as fs6 from "node:fs";
3986
- import * as path7 from "node:path";
3987
- import { fileURLToPath } from "node:url";
3988
- function readPackageVersion() {
3989
- try {
3990
- const moduleDir = path7.dirname(fileURLToPath(import.meta.url));
3991
- const candidates = [
3992
- path7.join(moduleDir, "package.json"),
3993
- // bundled: dist root
3994
- path7.join(moduleDir, "..", "package.json"),
3995
- path7.join(moduleDir, "..", "..", "package.json"),
3996
- // dev: src/services -> root
3997
- path7.join(moduleDir, "..", "..", "..", "package.json")
3998
- ];
3999
- for (const p of candidates) {
4000
- if (!fs6.existsSync(p)) continue;
4001
- const pkg = JSON.parse(fs6.readFileSync(p, "utf8"));
4002
- if (pkg.name === PACKAGE_NAME && pkg.version) return pkg.version;
4003
- }
4004
- } catch {
4005
- }
4006
- return null;
4007
- }
4008
- function getCurrentVersion() {
4009
- return readPackageVersion() ?? "0.0.0";
4010
- }
4011
4178
 
4012
4179
  // _apps/@onklave/agent-cli/src/services/daemon-state.service.ts
4013
- import * as fs7 from "fs";
4014
- import * as path8 from "path";
4015
- import * as os6 from "os";
4180
+ import * as fs9 from "fs";
4181
+ import * as path9 from "path";
4182
+ import * as os7 from "os";
4016
4183
  var VALID_TRANSITIONS = {
4017
4184
  installing: ["registered"],
4018
4185
  registered: ["starting"],
@@ -4023,10 +4190,10 @@ var VALID_TRANSITIONS = {
4023
4190
  stopped: ["starting"]
4024
4191
  };
4025
4192
  function defaultStateFilePath() {
4026
- return path8.join(os6.homedir(), ".config", "onklave", "daemon.state.json");
4193
+ return path9.join(os7.homedir(), ".config", "onklave", "daemon.state.json");
4027
4194
  }
4028
4195
  function defaultPidFilePath() {
4029
- return path8.join(os6.homedir(), ".config", "onklave", "daemon.pid");
4196
+ return path9.join(os7.homedir(), ".config", "onklave", "daemon.pid");
4030
4197
  }
4031
4198
  var DaemonStateError = class extends Error {
4032
4199
  constructor(message) {
@@ -4050,8 +4217,8 @@ var DaemonStateService = class {
4050
4217
  */
4051
4218
  static readPersisted(stateFile = defaultStateFilePath()) {
4052
4219
  try {
4053
- if (!fs7.existsSync(stateFile)) return null;
4054
- const raw = fs7.readFileSync(stateFile, "utf8");
4220
+ if (!fs9.existsSync(stateFile)) return null;
4221
+ const raw = fs9.readFileSync(stateFile, "utf8");
4055
4222
  const parsed = JSON.parse(raw);
4056
4223
  if (!parsed.state || !parsed.enteredAt) return null;
4057
4224
  return parsed;
@@ -4101,8 +4268,8 @@ var DaemonStateService = class {
4101
4268
  }
4102
4269
  }
4103
4270
  persist(reason) {
4104
- const dir = path8.dirname(this.stateFile);
4105
- fs7.mkdirSync(dir, { recursive: true });
4271
+ const dir = path9.dirname(this.stateFile);
4272
+ fs9.mkdirSync(dir, { recursive: true });
4106
4273
  const payload = {
4107
4274
  state: this.current,
4108
4275
  enteredAt: this.enteredAt.toISOString(),
@@ -4112,8 +4279,8 @@ var DaemonStateService = class {
4112
4279
  ...this.latestRuntime ? { runtime: this.latestRuntime } : {}
4113
4280
  };
4114
4281
  const tmp = `${this.stateFile}.tmp`;
4115
- fs7.writeFileSync(tmp, JSON.stringify(payload, null, 2));
4116
- fs7.renameSync(tmp, this.stateFile);
4282
+ fs9.writeFileSync(tmp, JSON.stringify(payload, null, 2));
4283
+ fs9.renameSync(tmp, this.stateFile);
4117
4284
  }
4118
4285
  /**
4119
4286
  * Publish the latest runtime snapshot. Persists to the state file
@@ -4131,7 +4298,7 @@ var DaemonStateService = class {
4131
4298
  */
4132
4299
  clearPersisted() {
4133
4300
  try {
4134
- fs7.unlinkSync(this.stateFile);
4301
+ fs9.unlinkSync(this.stateFile);
4135
4302
  } catch {
4136
4303
  }
4137
4304
  }
@@ -4375,7 +4542,9 @@ async function daemonStart() {
4375
4542
  platformUrl,
4376
4543
  deviceToken: creds.deviceToken,
4377
4544
  machineId: creds.machineId,
4378
- getActiveSessionCount: () => claimHandler.getActiveSessionCount()
4545
+ getActiveSessionCount: () => claimHandler.getActiveSessionCount(),
4546
+ cliVersion: readPackageVersion() ?? "0.0.0",
4547
+ posture: collectPosture()
4379
4548
  });
4380
4549
  const drainAndExit = async (reason) => {
4381
4550
  if (shuttingDown) return;
@@ -4438,6 +4607,35 @@ async function daemonStart() {
4438
4607
  }
4439
4608
  console.log("Connected.");
4440
4609
  claimHandler.attachToSocket(comms.inner());
4610
+ const upgradeService = new DaemonUpgradeService({
4611
+ allowAutoUpgrade: !!process.env["ONKLAVE_ALLOW_AUTO_UPGRADE"],
4612
+ currentVersion: readPackageVersion() ?? "0.0.0",
4613
+ runInstall: async () => {
4614
+ const { execFileSync } = __require("child_process");
4615
+ execFileSync("npm", ["install", "-g", "@onklave/agent-cli@latest"], {
4616
+ stdio: "pipe",
4617
+ timeout: 18e4
4618
+ });
4619
+ const v = execFileSync(
4620
+ "npm",
4621
+ ["view", "@onklave/agent-cli@latest", "version"],
4622
+ { stdio: "pipe", timeout: 3e4 }
4623
+ ).toString().trim();
4624
+ return v || "latest";
4625
+ },
4626
+ requestRestart: () => void drainAndExit("upgrade"),
4627
+ audit: (action, details) => auditStreamer.record({
4628
+ sessionId: creds.machineId,
4629
+ type: "daemon_lifecycle" /* DAEMON_LIFECYCLE */,
4630
+ action,
4631
+ details,
4632
+ outcome: action === "upgrade_failed" ? "failure" : "success"
4633
+ }),
4634
+ log: (m) => console.log(m)
4635
+ });
4636
+ comms.inner().onUpgradeRequest(() => {
4637
+ void upgradeService.handle();
4638
+ });
4441
4639
  await stateService.transition("online");
4442
4640
  heartbeat.start();
4443
4641
  resourceSampler.start();
@@ -4587,7 +4785,7 @@ function transitionToAction(next) {
4587
4785
  }
4588
4786
  function readPid(pidFile) {
4589
4787
  try {
4590
- const raw = fs8.readFileSync(pidFile, "utf8").trim();
4788
+ const raw = fs10.readFileSync(pidFile, "utf8").trim();
4591
4789
  const parsed = Number.parseInt(raw, 10);
4592
4790
  return Number.isFinite(parsed) && parsed > 0 ? parsed : null;
4593
4791
  } catch {
@@ -4596,12 +4794,12 @@ function readPid(pidFile) {
4596
4794
  }
4597
4795
  function writePid(pidFile) {
4598
4796
  const dir = pidFile.replace(/\/[^/]+$/, "");
4599
- fs8.mkdirSync(dir, { recursive: true });
4600
- fs8.writeFileSync(pidFile, String(process.pid), { mode: 384 });
4797
+ fs10.mkdirSync(dir, { recursive: true });
4798
+ fs10.writeFileSync(pidFile, String(process.pid), { mode: 384 });
4601
4799
  }
4602
4800
  function removePid(pidFile) {
4603
4801
  try {
4604
- fs8.unlinkSync(pidFile);
4802
+ fs10.unlinkSync(pidFile);
4605
4803
  } catch {
4606
4804
  }
4607
4805
  }
@@ -4645,15 +4843,15 @@ var COMMANDS = {
4645
4843
  };
4646
4844
 
4647
4845
  // _apps/@onklave/agent-cli/src/services/update-notifier.service.ts
4648
- import * as fs9 from "node:fs";
4649
- import * as path9 from "node:path";
4650
- import * as os7 from "node:os";
4846
+ import * as fs11 from "node:fs";
4847
+ import * as path10 from "node:path";
4848
+ import * as os8 from "node:os";
4651
4849
  import { createInterface } from "node:readline/promises";
4652
4850
  import { spawnSync } from "node:child_process";
4653
4851
  var CHECK_TTL_MS = 24 * 60 * 60 * 1e3;
4654
4852
  var FETCH_DEADLINE_MS = 1500;
4655
- var CACHE_PATH = path9.join(
4656
- os7.homedir(),
4853
+ var CACHE_PATH = path10.join(
4854
+ os8.homedir(),
4657
4855
  ".config",
4658
4856
  "onklave",
4659
4857
  "update-check.json"
@@ -4732,7 +4930,7 @@ async function maybeNotifyUpdate(deps = {}) {
4732
4930
  }
4733
4931
  }
4734
4932
  function withTimeout(promise, ms) {
4735
- return new Promise((resolve3, reject) => {
4933
+ return new Promise((resolve4, reject) => {
4736
4934
  const timer = setTimeout(
4737
4935
  () => reject(new Error("update check timed out")),
4738
4936
  ms
@@ -4741,7 +4939,7 @@ function withTimeout(promise, ms) {
4741
4939
  promise.then(
4742
4940
  (value) => {
4743
4941
  clearTimeout(timer);
4744
- resolve3(value);
4942
+ resolve4(value);
4745
4943
  },
4746
4944
  (err) => {
4747
4945
  clearTimeout(timer);
@@ -4763,16 +4961,16 @@ function isCacheFresh(cache, nowMs) {
4763
4961
  }
4764
4962
  function readCache() {
4765
4963
  try {
4766
- if (!fs9.existsSync(CACHE_PATH)) return {};
4767
- return JSON.parse(fs9.readFileSync(CACHE_PATH, "utf8"));
4964
+ if (!fs11.existsSync(CACHE_PATH)) return {};
4965
+ return JSON.parse(fs11.readFileSync(CACHE_PATH, "utf8"));
4768
4966
  } catch {
4769
4967
  return {};
4770
4968
  }
4771
4969
  }
4772
4970
  function writeCache(cache) {
4773
4971
  try {
4774
- fs9.mkdirSync(path9.dirname(CACHE_PATH), { recursive: true, mode: 448 });
4775
- fs9.writeFileSync(CACHE_PATH, JSON.stringify(cache, null, 2), "utf8");
4972
+ fs11.mkdirSync(path10.dirname(CACHE_PATH), { recursive: true, mode: 448 });
4973
+ fs11.writeFileSync(CACHE_PATH, JSON.stringify(cache, null, 2), "utf8");
4776
4974
  } catch {
4777
4975
  }
4778
4976
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onklave/agent-cli",
3
- "version": "0.1.44",
3
+ "version": "0.1.46",
4
4
  "description": "Onklave Agent CLI — local agent runner with cloud orchestration",
5
5
  "bin": {
6
6
  "onklave": "./main.js"