@onklave/agent-cli 0.1.45 → 0.1.47

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 +411 -210
  2. package/package.json +1 -1
package/main.js CHANGED
@@ -743,6 +743,19 @@ var PlatformClient = class {
743
743
  async listMachines() {
744
744
  return this.request("GET", "/api/v1/runner/machines");
745
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
+ }
746
759
  /*
747
760
  * Check platform health.
748
761
  */
@@ -761,8 +774,8 @@ var PlatformClient = class {
761
774
  /*
762
775
  * Make an authenticated HTTP request to the platform.
763
776
  */
764
- async request(method, path10, body, extraHeaders) {
765
- const url = `${this.baseUrl}${GATEWAY_SERVICE_PREFIX}${path10}`;
777
+ async request(method, path12, body, extraHeaders) {
778
+ const url = `${this.baseUrl}${GATEWAY_SERVICE_PREFIX}${path12}`;
766
779
  const headers = {
767
780
  Authorization: `Bearer ${this.token}`,
768
781
  "Content-Type": "application/json",
@@ -817,7 +830,7 @@ var CommsClient = class {
817
830
  * (e.g. `assignment:claim-available`) to this specific runner.
818
831
  */
819
832
  async connect(commsUrl, token, extra) {
820
- return new Promise((resolve3, reject) => {
833
+ return new Promise((resolve4, reject) => {
821
834
  const baseUrl = commsUrl.replace(/\/+$/, "");
822
835
  this.socket = io(`${baseUrl}/agent-cli`, {
823
836
  auth: {
@@ -842,7 +855,7 @@ var CommsClient = class {
842
855
  this.socket.on("connect", () => {
843
856
  clearTimeout(connectTimeout);
844
857
  this.reconnectAttempts = 0;
845
- resolve3();
858
+ resolve4();
846
859
  });
847
860
  this.socket.on("connect_error", (error) => {
848
861
  this.reconnectAttempts++;
@@ -928,6 +941,15 @@ var CommsClient = class {
928
941
  onAssignmentAvailable(handler) {
929
942
  this.socket?.on("assignment:claim-available", handler);
930
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
+ }
931
953
  };
932
954
 
933
955
  // _apps/@onklave/agent-cli/src/services/session-manager.ts
@@ -1018,16 +1040,16 @@ var SessionManager = class {
1018
1040
  throw new Error(`No active session found with ID: ${sessionId}`);
1019
1041
  }
1020
1042
  session.process.kill("SIGTERM");
1021
- await new Promise((resolve3) => {
1043
+ await new Promise((resolve4) => {
1022
1044
  const forceTimeout = setTimeout(() => {
1023
1045
  if (session.process.killed === false) {
1024
1046
  session.process.kill("SIGKILL");
1025
1047
  }
1026
- resolve3();
1048
+ resolve4();
1027
1049
  }, 5e3);
1028
1050
  session.process.on("exit", () => {
1029
1051
  clearTimeout(forceTimeout);
1030
- resolve3();
1052
+ resolve4();
1031
1053
  });
1032
1054
  });
1033
1055
  this.activeSessions.delete(sessionId);
@@ -1227,7 +1249,50 @@ var AuditStreamer = class {
1227
1249
  };
1228
1250
 
1229
1251
  // _apps/@onklave/agent-cli/src/services/guardrail-enforcer.ts
1252
+ import * as fs3 from "fs";
1230
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
+ }
1231
1296
  var GuardrailEnforcer = class {
1232
1297
  constructor(config) {
1233
1298
  this.config = config;
@@ -1286,11 +1351,11 @@ var GuardrailEnforcer = class {
1286
1351
  * Check if a file path is accessible for the given operation.
1287
1352
  */
1288
1353
  checkPathAccess(filePath, operation) {
1289
- const normalizedPath = path3.resolve(filePath);
1354
+ const realPath = safeRealpath(filePath);
1290
1355
  if (operation === "write" || operation === "delete") {
1291
1356
  if (this.config.writablePaths.length > 0) {
1292
1357
  const isWritable = this.config.writablePaths.some(
1293
- (wp) => normalizedPath.startsWith(path3.resolve(wp))
1358
+ (wp) => isPathWithin(filePath, wp)
1294
1359
  );
1295
1360
  if (!isWritable) {
1296
1361
  return {
@@ -1303,7 +1368,7 @@ var GuardrailEnforcer = class {
1303
1368
  if (operation === "read") {
1304
1369
  if (this.config.readablePaths.length > 0) {
1305
1370
  const isReadable = this.config.readablePaths.some(
1306
- (rp) => normalizedPath.startsWith(path3.resolve(rp))
1371
+ (rp) => isPathWithin(filePath, rp)
1307
1372
  );
1308
1373
  if (!isReadable) {
1309
1374
  return {
@@ -1322,9 +1387,10 @@ var GuardrailEnforcer = class {
1322
1387
  "id_rsa",
1323
1388
  "id_ed25519"
1324
1389
  ];
1325
- const basename3 = path3.basename(normalizedPath);
1390
+ const basename3 = path3.basename(realPath);
1391
+ const normForward = realPath.split(path3.sep).join("/");
1326
1392
  for (const sensitive of sensitivePatterns) {
1327
- if (basename3 === sensitive || normalizedPath.includes(`/${sensitive}`)) {
1393
+ if (basename3 === sensitive || normForward.includes(`/${sensitive}`)) {
1328
1394
  return {
1329
1395
  allowed: false,
1330
1396
  reason: `Access to sensitive path "${filePath}" is blocked by default`
@@ -1431,7 +1497,9 @@ var HeartbeatService = class {
1431
1497
  machineId: this.opts.machineId,
1432
1498
  status: "online",
1433
1499
  activeSessionCount: this.opts.getActiveSessionCount(),
1434
- resourceUsage: this.opts.getResourceUsage?.()
1500
+ resourceUsage: this.opts.getResourceUsage?.(),
1501
+ cliVersion: this.opts.cliVersion,
1502
+ posture: this.opts.posture
1435
1503
  };
1436
1504
  try {
1437
1505
  const response = await fetch(
@@ -1460,6 +1528,141 @@ var HeartbeatService = class {
1460
1528
  }
1461
1529
  };
1462
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
+
1463
1666
  // _apps/@onklave/agent-cli/src/tui/render.tsx
1464
1667
  import { render } from "ink";
1465
1668
 
@@ -2046,6 +2249,27 @@ async function runCommand(args) {
2046
2249
  resolvedConfig.platformUrl,
2047
2250
  creds.token
2048
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
+ }
2049
2273
  const sessionManager = new SessionManager();
2050
2274
  let heartbeat = null;
2051
2275
  if (creds.machineId && creds.deviceToken) {
@@ -2053,7 +2277,9 @@ async function runCommand(args) {
2053
2277
  platformUrl: resolvedConfig.platformUrl,
2054
2278
  deviceToken: creds.deviceToken,
2055
2279
  machineId: creds.machineId,
2056
- getActiveSessionCount: () => sessionManager.getActiveSessionIds().length
2280
+ getActiveSessionCount: () => sessionManager.getActiveSessionIds().length,
2281
+ cliVersion: getCurrentVersion(),
2282
+ posture: collectPosture()
2057
2283
  });
2058
2284
  heartbeat.start();
2059
2285
  const stopHeartbeat = () => heartbeat?.stop();
@@ -2135,8 +2361,8 @@ async function runCommand(args) {
2135
2361
  }
2136
2362
  const addDirs = [
2137
2363
  .../* @__PURE__ */ new Set([
2138
- ...resolvedConfig.writablePaths,
2139
- ...resolvedConfig.readablePaths
2364
+ path5.resolve(resolvedConfig.context),
2365
+ ...effectiveAllowedRoots
2140
2366
  ])
2141
2367
  ];
2142
2368
  const sessionConfig = {
@@ -2249,15 +2475,15 @@ async function runCommand(args) {
2249
2475
  }
2250
2476
  }
2251
2477
  });
2252
- await new Promise((resolve3) => {
2478
+ await new Promise((resolve4) => {
2253
2479
  const checkInterval = setInterval(() => {
2254
2480
  if (!sessionManager.isSessionActive(sessionId)) {
2255
2481
  clearInterval(checkInterval);
2256
- resolve3();
2482
+ resolve4();
2257
2483
  }
2258
2484
  }, 500);
2259
2485
  });
2260
- await new Promise((resolve3) => setTimeout(resolve3, 1e3));
2486
+ await new Promise((resolve4) => setTimeout(resolve4, 1e3));
2261
2487
  tuiInstance.unmount();
2262
2488
  } catch (err) {
2263
2489
  tuiInstance.unmount();
@@ -2363,11 +2589,11 @@ Session timed out after ${resolvedConfig.timeout} seconds.`
2363
2589
  commsClient.disconnect();
2364
2590
  }
2365
2591
  });
2366
- await new Promise((resolve3) => {
2592
+ await new Promise((resolve4) => {
2367
2593
  const checkInterval = setInterval(() => {
2368
2594
  if (!sessionManager.isSessionActive(sessionId)) {
2369
2595
  clearInterval(checkInterval);
2370
- resolve3();
2596
+ resolve4();
2371
2597
  }
2372
2598
  }, 500);
2373
2599
  });
@@ -2598,9 +2824,35 @@ async function denyCommand(args) {
2598
2824
  }
2599
2825
 
2600
2826
  // _apps/@onklave/agent-cli/src/commands/doctor.command.ts
2601
- import * as fs3 from "fs";
2602
- import * as path4 from "path";
2827
+ import * as fs6 from "fs";
2828
+ import * as path6 from "path";
2829
+
2830
+ // _apps/@onklave/agent-cli/src/services/command-exists.ts
2603
2831
  import { execSync } from "child_process";
2832
+ function lookupCommand(command) {
2833
+ return process.platform === "win32" ? `where ${command}` : `which ${command}`;
2834
+ }
2835
+ function commandExists(command) {
2836
+ try {
2837
+ execSync(lookupCommand(command), { stdio: "ignore", timeout: 3e3 });
2838
+ return true;
2839
+ } catch {
2840
+ return false;
2841
+ }
2842
+ }
2843
+ function commandPath(command) {
2844
+ try {
2845
+ const out = execSync(lookupCommand(command), {
2846
+ encoding: "utf-8",
2847
+ timeout: 5e3
2848
+ }).trim();
2849
+ return out.split(/\r?\n/)[0] || null;
2850
+ } catch {
2851
+ return null;
2852
+ }
2853
+ }
2854
+
2855
+ // _apps/@onklave/agent-cli/src/commands/doctor.command.ts
2604
2856
  async function doctorCommand() {
2605
2857
  console.log("Onklave Agent CLI \u2014 Doctor\n");
2606
2858
  console.log("Running diagnostics...\n");
@@ -2669,23 +2921,19 @@ async function checkPlatform() {
2669
2921
  };
2670
2922
  }
2671
2923
  function checkClaudeCli() {
2672
- try {
2673
- const claudePath = execSync("which claude", {
2674
- encoding: "utf-8",
2675
- timeout: 5e3
2676
- }).trim();
2924
+ const claudePath = commandPath("claude");
2925
+ if (claudePath) {
2677
2926
  return {
2678
2927
  name: "Claude Code CLI",
2679
2928
  status: "ok",
2680
2929
  detail: `Found at ${claudePath}`
2681
2930
  };
2682
- } catch {
2683
- return {
2684
- name: "Claude Code CLI",
2685
- status: "fail",
2686
- detail: "Not found. Install Claude Code CLI: https://docs.anthropic.com/claude-code"
2687
- };
2688
2931
  }
2932
+ return {
2933
+ name: "Claude Code CLI",
2934
+ status: "fail",
2935
+ detail: "Not found. Install Claude Code CLI: https://docs.anthropic.com/claude-code"
2936
+ };
2689
2937
  }
2690
2938
  function checkNodeVersion() {
2691
2939
  const version = process.version;
@@ -2707,10 +2955,10 @@ function checkNodeVersion() {
2707
2955
  };
2708
2956
  }
2709
2957
  function checkProjectConfig() {
2710
- const configPath = path4.join(process.cwd(), ".onklave.json");
2711
- if (fs3.existsSync(configPath)) {
2958
+ const configPath = path6.join(process.cwd(), ".onklave.json");
2959
+ if (fs6.existsSync(configPath)) {
2712
2960
  try {
2713
- const raw = fs3.readFileSync(configPath, "utf-8");
2961
+ const raw = fs6.readFileSync(configPath, "utf-8");
2714
2962
  JSON.parse(raw);
2715
2963
  return {
2716
2964
  name: "Project config",
@@ -2760,18 +3008,18 @@ async function checkWebSocket() {
2760
3008
  }
2761
3009
 
2762
3010
  // _apps/@onklave/agent-cli/src/commands/init.command.ts
2763
- import * as fs4 from "fs";
2764
- import * as path5 from "path";
3011
+ import * as fs7 from "fs";
3012
+ import * as path7 from "path";
2765
3013
  async function initCommand() {
2766
- const configPath = path5.join(process.cwd(), ".onklave.json");
2767
- if (fs4.existsSync(configPath)) {
3014
+ const configPath = path7.join(process.cwd(), ".onklave.json");
3015
+ if (fs7.existsSync(configPath)) {
2768
3016
  console.error("Error: .onklave.json already exists in this directory.");
2769
3017
  console.log("To overwrite, delete the existing file first.");
2770
3018
  process.exitCode = 1;
2771
3019
  return;
2772
3020
  }
2773
3021
  const template = {
2774
- project: path5.basename(process.cwd()),
3022
+ project: path7.basename(process.cwd()),
2775
3023
  org: "",
2776
3024
  description: "",
2777
3025
  defaults: {
@@ -2806,7 +3054,7 @@ async function initCommand() {
2806
3054
  system_prompt_append: ""
2807
3055
  }
2808
3056
  };
2809
- fs4.writeFileSync(
3057
+ fs7.writeFileSync(
2810
3058
  configPath,
2811
3059
  JSON.stringify(template, null, 2) + "\n",
2812
3060
  "utf-8"
@@ -2893,7 +3141,7 @@ async function configCommand(args) {
2893
3141
  }
2894
3142
 
2895
3143
  // _apps/@onklave/agent-cli/src/commands/register.command.ts
2896
- import * as os3 from "os";
3144
+ import * as os4 from "os";
2897
3145
  async function registerCommand(args) {
2898
3146
  const { flags } = parseArgs(args);
2899
3147
  const refresh = flags["refresh"] === true;
@@ -2940,11 +3188,13 @@ Machine ${creds.machineId} updated successfully.`);
2940
3188
  return;
2941
3189
  }
2942
3190
  const metadata = {
2943
- hostname: os3.hostname(),
2944
- os: `${os3.platform()} ${os3.release()}`,
2945
- arch: os3.arch(),
3191
+ hostname: os4.hostname(),
3192
+ os: `${os4.platform()} ${os4.release()}`,
3193
+ arch: os4.arch(),
2946
3194
  nodeVersion: process.version,
2947
- capabilities: detectCapabilities()
3195
+ cliVersion: getCurrentVersion(),
3196
+ capabilities: detectCapabilities(),
3197
+ posture: collectPosture()
2948
3198
  };
2949
3199
  try {
2950
3200
  console.log("Registering machine with Onklave platform...");
@@ -2982,17 +3232,11 @@ This machine can now receive remote agent session requests.`
2982
3232
  }
2983
3233
  function detectCapabilities() {
2984
3234
  const capabilities = ["agent-runner"];
2985
- try {
2986
- const { execSync: execSync2 } = __require("child_process");
2987
- execSync2("which claude", { timeout: 3e3, stdio: "pipe" });
3235
+ if (commandExists("claude")) {
2988
3236
  capabilities.push("claude-code");
2989
- } catch {
2990
3237
  }
2991
- try {
2992
- const { execSync: execSync2 } = __require("child_process");
2993
- execSync2("which docker", { timeout: 3e3, stdio: "pipe" });
3238
+ if (commandExists("docker")) {
2994
3239
  capabilities.push("docker");
2995
- } catch {
2996
3240
  }
2997
3241
  return capabilities;
2998
3242
  }
@@ -3035,7 +3279,8 @@ async function logsCommand(args) {
3035
3279
  }
3036
3280
 
3037
3281
  // _apps/@onklave/agent-cli/src/commands/daemon.command.ts
3038
- import * as fs8 from "fs";
3282
+ import * as fs10 from "fs";
3283
+ import * as path10 from "path";
3039
3284
 
3040
3285
  // _apps/@onklave/agent-cli/src/services/daemon-comms.service.ts
3041
3286
  var DaemonCommsService = class {
@@ -3109,7 +3354,7 @@ var DaemonCommsService = class {
3109
3354
  }
3110
3355
  };
3111
3356
  function defaultSleep(ms) {
3112
- return new Promise((resolve3) => setTimeout(resolve3, ms));
3357
+ return new Promise((resolve4) => setTimeout(resolve4, ms));
3113
3358
  }
3114
3359
 
3115
3360
  // _apps/@onklave/agent-cli/src/services/daemon-claim-handler.service.ts
@@ -3462,8 +3707,8 @@ var PlatformBrokerClient = class {
3462
3707
  params
3463
3708
  );
3464
3709
  }
3465
- async request(method, path10, body) {
3466
- const url = `${this.baseUrl}/agent-orchestration${path10}`;
3710
+ async request(method, path12, body) {
3711
+ const url = `${this.baseUrl}/agent-orchestration${path12}`;
3467
3712
  const response = await fetch(url, {
3468
3713
  method,
3469
3714
  headers: {
@@ -3499,9 +3744,9 @@ var PlatformBrokerClient = class {
3499
3744
 
3500
3745
  // _apps/@onklave/agent-cli/src/services/work-item-runner.service.ts
3501
3746
  import { spawn as spawn2 } from "child_process";
3502
- import * as fs5 from "fs";
3503
- import * as os4 from "os";
3504
- import * as path6 from "path";
3747
+ import * as fs8 from "fs";
3748
+ import * as os5 from "os";
3749
+ import * as path8 from "path";
3505
3750
  var WorkItemRunner = class {
3506
3751
  constructor(opts = {}) {
3507
3752
  this.sessionManager = opts.sessionManager ?? new SessionManager();
@@ -3522,10 +3767,10 @@ var WorkItemRunner = class {
3522
3767
  summary: `Work item ${workItem.id} has no clonable repo (project.repos[0].url missing).`
3523
3768
  };
3524
3769
  }
3525
- const workDir = fs5.mkdtempSync(
3526
- path6.join(os4.tmpdir(), `onklave-pickup-${sessionId}-`)
3770
+ const workDir = fs8.mkdtempSync(
3771
+ path8.join(os5.tmpdir(), `onklave-pickup-${sessionId}-`)
3527
3772
  );
3528
- const repoDir = path6.join(workDir, sanitizeName(repo.name) || "repo");
3773
+ const repoDir = path8.join(workDir, sanitizeName(repo.name) || "repo");
3529
3774
  const branchName = `onklave/work-item/${workItem.id}`;
3530
3775
  try {
3531
3776
  await this.git(["clone", repo.url, repoDir], workDir);
@@ -3577,7 +3822,7 @@ var WorkItemRunner = class {
3577
3822
  systemPromptAppend: null
3578
3823
  };
3579
3824
  let output = "";
3580
- const exitCode = await new Promise((resolve3) => {
3825
+ const exitCode = await new Promise((resolve4) => {
3581
3826
  const timeout = setTimeout(() => {
3582
3827
  void this.sessionManager.stopSession(sessionId).catch(() => void 0);
3583
3828
  }, this.timeoutSeconds * 1e3);
@@ -3590,7 +3835,7 @@ var WorkItemRunner = class {
3590
3835
  },
3591
3836
  onExit: (code) => {
3592
3837
  clearTimeout(timeout);
3593
- resolve3(code);
3838
+ resolve4(code);
3594
3839
  }
3595
3840
  });
3596
3841
  });
@@ -3623,12 +3868,12 @@ function sanitizeName(name) {
3623
3868
  }
3624
3869
  function cleanup(dir) {
3625
3870
  try {
3626
- fs5.rmSync(dir, { recursive: true, force: true });
3871
+ fs8.rmSync(dir, { recursive: true, force: true });
3627
3872
  } catch {
3628
3873
  }
3629
3874
  }
3630
3875
  function defaultGit(args, cwd) {
3631
- return new Promise((resolve3, reject) => {
3876
+ return new Promise((resolve4, reject) => {
3632
3877
  const child = spawn2("git", args, {
3633
3878
  cwd,
3634
3879
  stdio: ["ignore", "pipe", "pipe"]
@@ -3640,7 +3885,7 @@ function defaultGit(args, cwd) {
3640
3885
  child.on("error", (err) => reject(err));
3641
3886
  child.on("exit", (code) => {
3642
3887
  if (code === 0) {
3643
- resolve3();
3888
+ resolve4();
3644
3889
  } else {
3645
3890
  reject(new Error(`git ${args[0]} exited ${code}: ${stderr.trim()}`));
3646
3891
  }
@@ -3649,7 +3894,7 @@ function defaultGit(args, cwd) {
3649
3894
  }
3650
3895
 
3651
3896
  // _apps/@onklave/agent-cli/src/services/daemon-resource-sampler.service.ts
3652
- import * as os5 from "os";
3897
+ import * as os6 from "os";
3653
3898
  var DEFAULT_SAMPLE_INTERVAL_MS = 5e3;
3654
3899
  var DEFAULT_WINDOW_MS = 6e4;
3655
3900
  var DaemonResourceSampler = class {
@@ -3717,8 +3962,8 @@ var DaemonResourceSampler = class {
3717
3962
  }
3718
3963
  };
3719
3964
  function defaultCpuPercent() {
3720
- const cpus2 = os5.cpus().length || 1;
3721
- const load1m = os5.loadavg()[0];
3965
+ const cpus2 = os6.cpus().length || 1;
3966
+ const load1m = os6.loadavg()[0];
3722
3967
  return load1m / cpus2 * 100;
3723
3968
  }
3724
3969
  function defaultMemoryRss() {
@@ -3749,7 +3994,7 @@ var DaemonSpawner = class {
3749
3994
  orgId: req.orgId,
3750
3995
  systemPromptAppend: null
3751
3996
  };
3752
- return new Promise((resolve3, reject) => {
3997
+ return new Promise((resolve4, reject) => {
3753
3998
  void this.sessionManager.spawnSession(req.sessionId, config, {
3754
3999
  onStdout: (data) => {
3755
4000
  process.stdout.write(data);
@@ -3759,7 +4004,7 @@ var DaemonSpawner = class {
3759
4004
  },
3760
4005
  onExit: (code, signal) => {
3761
4006
  if (code === 0) {
3762
- resolve3();
4007
+ resolve4();
3763
4008
  } else {
3764
4009
  reject(
3765
4010
  new Error(
@@ -3910,123 +4155,48 @@ var DaemonTokenRefresher = class {
3910
4155
  }
3911
4156
  };
3912
4157
 
3913
- // _apps/@onklave/agent-cli/src/services/daemon-update-checker.service.ts
3914
- var REGISTRY_URL = "https://registry.npmjs.org";
3915
- var PACKAGE_NAME = "@onklave/agent-cli";
3916
- var DaemonUpdateChecker = class {
4158
+ // _apps/@onklave/agent-cli/src/services/daemon-upgrade.service.ts
4159
+ var DaemonUpgradeService = class {
3917
4160
  constructor(opts) {
3918
- this.latestVersion = null;
3919
- this.lastCheckedAt = null;
3920
- this.emittedForVersion = null;
3921
- this.currentVersion = opts.currentVersion;
3922
- this.audit = opts.audit;
3923
- this.registryFetcher = opts.registryFetcher ?? defaultRegistryFetcher;
4161
+ this.opts = opts;
3924
4162
  }
3925
- /**
3926
- * Hit the registry. On success, sets latestVersion and (if newer than
3927
- * current) emits `daemon.update_available` — exactly once per version,
3928
- * even across repeated checks. On failure, logs and returns; the next
3929
- * tick can retry.
3930
- */
3931
- async check() {
4163
+ async handle() {
4164
+ const { allowAutoUpgrade, currentVersion } = this.opts;
4165
+ if (!allowAutoUpgrade) {
4166
+ this.opts.audit("upgrade_requested_denied", {
4167
+ reason: "auto_upgrade_disabled",
4168
+ currentVersion
4169
+ });
4170
+ this.opts.log?.(
4171
+ "[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"
4172
+ );
4173
+ return;
4174
+ }
4175
+ this.opts.audit("upgrade_started", { from: currentVersion });
3932
4176
  try {
3933
- const latest = await this.registryFetcher(PACKAGE_NAME);
3934
- this.latestVersion = latest;
3935
- this.lastCheckedAt = /* @__PURE__ */ new Date();
3936
- if (isNewerVersion(latest, this.currentVersion) && this.emittedForVersion !== latest) {
3937
- this.audit.record({
3938
- sessionId: this.currentVersion,
3939
- type: "daemon_lifecycle" /* DAEMON_LIFECYCLE */,
3940
- action: DAEMON_AUDIT_ACTIONS.UPDATE_AVAILABLE,
3941
- details: {
3942
- currentVersion: this.currentVersion,
3943
- latestVersion: latest
3944
- },
3945
- outcome: "success"
3946
- });
3947
- this.emittedForVersion = latest;
3948
- }
3949
- } catch (err) {
3950
- console.warn(
3951
- `[daemon] update check failed: ${err.message} (will retry)`
4177
+ const newVersion = await this.opts.runInstall();
4178
+ this.opts.audit("upgrade_completed", {
4179
+ from: currentVersion,
4180
+ to: newVersion
4181
+ });
4182
+ this.opts.log?.(
4183
+ `[daemon] upgraded ${currentVersion} -> ${newVersion}; restarting for supervisor pickup.`
3952
4184
  );
4185
+ this.opts.requestRestart();
4186
+ } catch (err) {
4187
+ this.opts.audit("upgrade_failed", {
4188
+ from: currentVersion,
4189
+ error: err.message
4190
+ });
4191
+ this.opts.log?.(`[daemon] upgrade failed: ${err.message}`);
3953
4192
  }
3954
4193
  }
3955
- info() {
3956
- return {
3957
- currentVersion: this.currentVersion,
3958
- latestVersion: this.latestVersion,
3959
- updateAvailable: !!this.latestVersion && isNewerVersion(this.latestVersion, this.currentVersion),
3960
- checkedAt: this.lastCheckedAt?.toISOString() ?? null
3961
- };
3962
- }
3963
4194
  };
3964
- async function defaultRegistryFetcher(packageName) {
3965
- const url = `${REGISTRY_URL}/${encodeURIComponent(packageName)}/latest`;
3966
- const response = await fetch(url, {
3967
- method: "GET",
3968
- headers: { Accept: "application/json" },
3969
- signal: AbortSignal.timeout(1e4)
3970
- });
3971
- if (!response.ok) {
3972
- throw new Error(`registry ${response.status} ${response.statusText}`);
3973
- }
3974
- const body = await response.json();
3975
- if (!body.version) throw new Error("registry response missing version");
3976
- return body.version;
3977
- }
3978
- function isNewerVersion(a, b) {
3979
- const pa = parseSemverNumeric(a);
3980
- const pb = parseSemverNumeric(b);
3981
- if (!pa || !pb) return false;
3982
- for (let i = 0; i < 3; i += 1) {
3983
- if (pa[i] > pb[i]) return true;
3984
- if (pa[i] < pb[i]) return false;
3985
- }
3986
- return false;
3987
- }
3988
- function parseSemverNumeric(v) {
3989
- const match = /^(\d+)\.(\d+)\.(\d+)/.exec(v);
3990
- if (!match) return null;
3991
- return [
3992
- Number.parseInt(match[1], 10),
3993
- Number.parseInt(match[2], 10),
3994
- Number.parseInt(match[3], 10)
3995
- ];
3996
- }
3997
-
3998
- // _apps/@onklave/agent-cli/src/services/cli-version.ts
3999
- import * as fs6 from "node:fs";
4000
- import * as path7 from "node:path";
4001
- import { fileURLToPath } from "node:url";
4002
- function readPackageVersion() {
4003
- try {
4004
- const moduleDir = path7.dirname(fileURLToPath(import.meta.url));
4005
- const candidates = [
4006
- path7.join(moduleDir, "package.json"),
4007
- // bundled: dist root
4008
- path7.join(moduleDir, "..", "package.json"),
4009
- path7.join(moduleDir, "..", "..", "package.json"),
4010
- // dev: src/services -> root
4011
- path7.join(moduleDir, "..", "..", "..", "package.json")
4012
- ];
4013
- for (const p of candidates) {
4014
- if (!fs6.existsSync(p)) continue;
4015
- const pkg = JSON.parse(fs6.readFileSync(p, "utf8"));
4016
- if (pkg.name === PACKAGE_NAME && pkg.version) return pkg.version;
4017
- }
4018
- } catch {
4019
- }
4020
- return null;
4021
- }
4022
- function getCurrentVersion() {
4023
- return readPackageVersion() ?? "0.0.0";
4024
- }
4025
4195
 
4026
4196
  // _apps/@onklave/agent-cli/src/services/daemon-state.service.ts
4027
- import * as fs7 from "fs";
4028
- import * as path8 from "path";
4029
- import * as os6 from "os";
4197
+ import * as fs9 from "fs";
4198
+ import * as path9 from "path";
4199
+ import * as os7 from "os";
4030
4200
  var VALID_TRANSITIONS = {
4031
4201
  installing: ["registered"],
4032
4202
  registered: ["starting"],
@@ -4037,10 +4207,10 @@ var VALID_TRANSITIONS = {
4037
4207
  stopped: ["starting"]
4038
4208
  };
4039
4209
  function defaultStateFilePath() {
4040
- return path8.join(os6.homedir(), ".config", "onklave", "daemon.state.json");
4210
+ return path9.join(os7.homedir(), ".config", "onklave", "daemon.state.json");
4041
4211
  }
4042
4212
  function defaultPidFilePath() {
4043
- return path8.join(os6.homedir(), ".config", "onklave", "daemon.pid");
4213
+ return path9.join(os7.homedir(), ".config", "onklave", "daemon.pid");
4044
4214
  }
4045
4215
  var DaemonStateError = class extends Error {
4046
4216
  constructor(message) {
@@ -4064,8 +4234,8 @@ var DaemonStateService = class {
4064
4234
  */
4065
4235
  static readPersisted(stateFile = defaultStateFilePath()) {
4066
4236
  try {
4067
- if (!fs7.existsSync(stateFile)) return null;
4068
- const raw = fs7.readFileSync(stateFile, "utf8");
4237
+ if (!fs9.existsSync(stateFile)) return null;
4238
+ const raw = fs9.readFileSync(stateFile, "utf8");
4069
4239
  const parsed = JSON.parse(raw);
4070
4240
  if (!parsed.state || !parsed.enteredAt) return null;
4071
4241
  return parsed;
@@ -4115,8 +4285,8 @@ var DaemonStateService = class {
4115
4285
  }
4116
4286
  }
4117
4287
  persist(reason) {
4118
- const dir = path8.dirname(this.stateFile);
4119
- fs7.mkdirSync(dir, { recursive: true });
4288
+ const dir = path9.dirname(this.stateFile);
4289
+ fs9.mkdirSync(dir, { recursive: true });
4120
4290
  const payload = {
4121
4291
  state: this.current,
4122
4292
  enteredAt: this.enteredAt.toISOString(),
@@ -4126,8 +4296,8 @@ var DaemonStateService = class {
4126
4296
  ...this.latestRuntime ? { runtime: this.latestRuntime } : {}
4127
4297
  };
4128
4298
  const tmp = `${this.stateFile}.tmp`;
4129
- fs7.writeFileSync(tmp, JSON.stringify(payload, null, 2));
4130
- fs7.renameSync(tmp, this.stateFile);
4299
+ fs9.writeFileSync(tmp, JSON.stringify(payload, null, 2));
4300
+ fs9.renameSync(tmp, this.stateFile);
4131
4301
  }
4132
4302
  /**
4133
4303
  * Publish the latest runtime snapshot. Persists to the state file
@@ -4145,7 +4315,7 @@ var DaemonStateService = class {
4145
4315
  */
4146
4316
  clearPersisted() {
4147
4317
  try {
4148
- fs7.unlinkSync(this.stateFile);
4318
+ fs9.unlinkSync(this.stateFile);
4149
4319
  } catch {
4150
4320
  }
4151
4321
  }
@@ -4389,7 +4559,9 @@ async function daemonStart() {
4389
4559
  platformUrl,
4390
4560
  deviceToken: creds.deviceToken,
4391
4561
  machineId: creds.machineId,
4392
- getActiveSessionCount: () => claimHandler.getActiveSessionCount()
4562
+ getActiveSessionCount: () => claimHandler.getActiveSessionCount(),
4563
+ cliVersion: readPackageVersion() ?? "0.0.0",
4564
+ posture: collectPosture()
4393
4565
  });
4394
4566
  const drainAndExit = async (reason) => {
4395
4567
  if (shuttingDown) return;
@@ -4452,6 +4624,35 @@ async function daemonStart() {
4452
4624
  }
4453
4625
  console.log("Connected.");
4454
4626
  claimHandler.attachToSocket(comms.inner());
4627
+ const upgradeService = new DaemonUpgradeService({
4628
+ allowAutoUpgrade: !!process.env["ONKLAVE_ALLOW_AUTO_UPGRADE"],
4629
+ currentVersion: readPackageVersion() ?? "0.0.0",
4630
+ runInstall: async () => {
4631
+ const { execFileSync } = __require("child_process");
4632
+ execFileSync("npm", ["install", "-g", "@onklave/agent-cli@latest"], {
4633
+ stdio: "pipe",
4634
+ timeout: 18e4
4635
+ });
4636
+ const v = execFileSync(
4637
+ "npm",
4638
+ ["view", "@onklave/agent-cli@latest", "version"],
4639
+ { stdio: "pipe", timeout: 3e4 }
4640
+ ).toString().trim();
4641
+ return v || "latest";
4642
+ },
4643
+ requestRestart: () => void drainAndExit("upgrade"),
4644
+ audit: (action, details) => auditStreamer.record({
4645
+ sessionId: creds.machineId,
4646
+ type: "daemon_lifecycle" /* DAEMON_LIFECYCLE */,
4647
+ action,
4648
+ details,
4649
+ outcome: action === "upgrade_failed" ? "failure" : "success"
4650
+ }),
4651
+ log: (m) => console.log(m)
4652
+ });
4653
+ comms.inner().onUpgradeRequest(() => {
4654
+ void upgradeService.handle();
4655
+ });
4455
4656
  await stateService.transition("online");
4456
4657
  heartbeat.start();
4457
4658
  resourceSampler.start();
@@ -4601,7 +4802,7 @@ function transitionToAction(next) {
4601
4802
  }
4602
4803
  function readPid(pidFile) {
4603
4804
  try {
4604
- const raw = fs8.readFileSync(pidFile, "utf8").trim();
4805
+ const raw = fs10.readFileSync(pidFile, "utf8").trim();
4605
4806
  const parsed = Number.parseInt(raw, 10);
4606
4807
  return Number.isFinite(parsed) && parsed > 0 ? parsed : null;
4607
4808
  } catch {
@@ -4609,13 +4810,13 @@ function readPid(pidFile) {
4609
4810
  }
4610
4811
  }
4611
4812
  function writePid(pidFile) {
4612
- const dir = pidFile.replace(/\/[^/]+$/, "");
4613
- fs8.mkdirSync(dir, { recursive: true });
4614
- fs8.writeFileSync(pidFile, String(process.pid), { mode: 384 });
4813
+ const dir = path10.dirname(pidFile);
4814
+ fs10.mkdirSync(dir, { recursive: true });
4815
+ fs10.writeFileSync(pidFile, String(process.pid), { mode: 384 });
4615
4816
  }
4616
4817
  function removePid(pidFile) {
4617
4818
  try {
4618
- fs8.unlinkSync(pidFile);
4819
+ fs10.unlinkSync(pidFile);
4619
4820
  } catch {
4620
4821
  }
4621
4822
  }
@@ -4659,15 +4860,15 @@ var COMMANDS = {
4659
4860
  };
4660
4861
 
4661
4862
  // _apps/@onklave/agent-cli/src/services/update-notifier.service.ts
4662
- import * as fs9 from "node:fs";
4663
- import * as path9 from "node:path";
4664
- import * as os7 from "node:os";
4863
+ import * as fs11 from "node:fs";
4864
+ import * as path11 from "node:path";
4865
+ import * as os8 from "node:os";
4665
4866
  import { createInterface } from "node:readline/promises";
4666
4867
  import { spawnSync } from "node:child_process";
4667
4868
  var CHECK_TTL_MS = 24 * 60 * 60 * 1e3;
4668
4869
  var FETCH_DEADLINE_MS = 1500;
4669
- var CACHE_PATH = path9.join(
4670
- os7.homedir(),
4870
+ var CACHE_PATH = path11.join(
4871
+ os8.homedir(),
4671
4872
  ".config",
4672
4873
  "onklave",
4673
4874
  "update-check.json"
@@ -4746,7 +4947,7 @@ async function maybeNotifyUpdate(deps = {}) {
4746
4947
  }
4747
4948
  }
4748
4949
  function withTimeout(promise, ms) {
4749
- return new Promise((resolve3, reject) => {
4950
+ return new Promise((resolve4, reject) => {
4750
4951
  const timer = setTimeout(
4751
4952
  () => reject(new Error("update check timed out")),
4752
4953
  ms
@@ -4755,7 +4956,7 @@ function withTimeout(promise, ms) {
4755
4956
  promise.then(
4756
4957
  (value) => {
4757
4958
  clearTimeout(timer);
4758
- resolve3(value);
4959
+ resolve4(value);
4759
4960
  },
4760
4961
  (err) => {
4761
4962
  clearTimeout(timer);
@@ -4777,16 +4978,16 @@ function isCacheFresh(cache, nowMs) {
4777
4978
  }
4778
4979
  function readCache() {
4779
4980
  try {
4780
- if (!fs9.existsSync(CACHE_PATH)) return {};
4781
- return JSON.parse(fs9.readFileSync(CACHE_PATH, "utf8"));
4981
+ if (!fs11.existsSync(CACHE_PATH)) return {};
4982
+ return JSON.parse(fs11.readFileSync(CACHE_PATH, "utf8"));
4782
4983
  } catch {
4783
4984
  return {};
4784
4985
  }
4785
4986
  }
4786
4987
  function writeCache(cache) {
4787
4988
  try {
4788
- fs9.mkdirSync(path9.dirname(CACHE_PATH), { recursive: true, mode: 448 });
4789
- fs9.writeFileSync(CACHE_PATH, JSON.stringify(cache, null, 2), "utf8");
4989
+ fs11.mkdirSync(path11.dirname(CACHE_PATH), { recursive: true, mode: 448 });
4990
+ fs11.writeFileSync(CACHE_PATH, JSON.stringify(cache, null, 2), "utf8");
4790
4991
  } catch {
4791
4992
  }
4792
4993
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onklave/agent-cli",
3
- "version": "0.1.45",
3
+ "version": "0.1.47",
4
4
  "description": "Onklave Agent CLI — local agent runner with cloud orchestration",
5
5
  "bin": {
6
6
  "onklave": "./main.js"