@onklave/agent-cli 0.1.45 → 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 +374 -190
  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, path11, body, extraHeaders) {
778
+ const url = `${this.baseUrl}${GATEWAY_SERVICE_PREFIX}${path11}`;
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,8 +2824,8 @@ 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";
2603
2829
  import { execSync } from "child_process";
2604
2830
  async function doctorCommand() {
2605
2831
  console.log("Onklave Agent CLI \u2014 Doctor\n");
@@ -2707,10 +2933,10 @@ function checkNodeVersion() {
2707
2933
  };
2708
2934
  }
2709
2935
  function checkProjectConfig() {
2710
- const configPath = path4.join(process.cwd(), ".onklave.json");
2711
- if (fs3.existsSync(configPath)) {
2936
+ const configPath = path6.join(process.cwd(), ".onklave.json");
2937
+ if (fs6.existsSync(configPath)) {
2712
2938
  try {
2713
- const raw = fs3.readFileSync(configPath, "utf-8");
2939
+ const raw = fs6.readFileSync(configPath, "utf-8");
2714
2940
  JSON.parse(raw);
2715
2941
  return {
2716
2942
  name: "Project config",
@@ -2760,18 +2986,18 @@ async function checkWebSocket() {
2760
2986
  }
2761
2987
 
2762
2988
  // _apps/@onklave/agent-cli/src/commands/init.command.ts
2763
- import * as fs4 from "fs";
2764
- import * as path5 from "path";
2989
+ import * as fs7 from "fs";
2990
+ import * as path7 from "path";
2765
2991
  async function initCommand() {
2766
- const configPath = path5.join(process.cwd(), ".onklave.json");
2767
- if (fs4.existsSync(configPath)) {
2992
+ const configPath = path7.join(process.cwd(), ".onklave.json");
2993
+ if (fs7.existsSync(configPath)) {
2768
2994
  console.error("Error: .onklave.json already exists in this directory.");
2769
2995
  console.log("To overwrite, delete the existing file first.");
2770
2996
  process.exitCode = 1;
2771
2997
  return;
2772
2998
  }
2773
2999
  const template = {
2774
- project: path5.basename(process.cwd()),
3000
+ project: path7.basename(process.cwd()),
2775
3001
  org: "",
2776
3002
  description: "",
2777
3003
  defaults: {
@@ -2806,7 +3032,7 @@ async function initCommand() {
2806
3032
  system_prompt_append: ""
2807
3033
  }
2808
3034
  };
2809
- fs4.writeFileSync(
3035
+ fs7.writeFileSync(
2810
3036
  configPath,
2811
3037
  JSON.stringify(template, null, 2) + "\n",
2812
3038
  "utf-8"
@@ -2893,7 +3119,7 @@ async function configCommand(args) {
2893
3119
  }
2894
3120
 
2895
3121
  // _apps/@onklave/agent-cli/src/commands/register.command.ts
2896
- import * as os3 from "os";
3122
+ import * as os4 from "os";
2897
3123
  async function registerCommand(args) {
2898
3124
  const { flags } = parseArgs(args);
2899
3125
  const refresh = flags["refresh"] === true;
@@ -2940,11 +3166,13 @@ Machine ${creds.machineId} updated successfully.`);
2940
3166
  return;
2941
3167
  }
2942
3168
  const metadata = {
2943
- hostname: os3.hostname(),
2944
- os: `${os3.platform()} ${os3.release()}`,
2945
- arch: os3.arch(),
3169
+ hostname: os4.hostname(),
3170
+ os: `${os4.platform()} ${os4.release()}`,
3171
+ arch: os4.arch(),
2946
3172
  nodeVersion: process.version,
2947
- capabilities: detectCapabilities()
3173
+ cliVersion: getCurrentVersion(),
3174
+ capabilities: detectCapabilities(),
3175
+ posture: collectPosture()
2948
3176
  };
2949
3177
  try {
2950
3178
  console.log("Registering machine with Onklave platform...");
@@ -3035,7 +3263,7 @@ async function logsCommand(args) {
3035
3263
  }
3036
3264
 
3037
3265
  // _apps/@onklave/agent-cli/src/commands/daemon.command.ts
3038
- import * as fs8 from "fs";
3266
+ import * as fs10 from "fs";
3039
3267
 
3040
3268
  // _apps/@onklave/agent-cli/src/services/daemon-comms.service.ts
3041
3269
  var DaemonCommsService = class {
@@ -3109,7 +3337,7 @@ var DaemonCommsService = class {
3109
3337
  }
3110
3338
  };
3111
3339
  function defaultSleep(ms) {
3112
- return new Promise((resolve3) => setTimeout(resolve3, ms));
3340
+ return new Promise((resolve4) => setTimeout(resolve4, ms));
3113
3341
  }
3114
3342
 
3115
3343
  // _apps/@onklave/agent-cli/src/services/daemon-claim-handler.service.ts
@@ -3462,8 +3690,8 @@ var PlatformBrokerClient = class {
3462
3690
  params
3463
3691
  );
3464
3692
  }
3465
- async request(method, path10, body) {
3466
- const url = `${this.baseUrl}/agent-orchestration${path10}`;
3693
+ async request(method, path11, body) {
3694
+ const url = `${this.baseUrl}/agent-orchestration${path11}`;
3467
3695
  const response = await fetch(url, {
3468
3696
  method,
3469
3697
  headers: {
@@ -3499,9 +3727,9 @@ var PlatformBrokerClient = class {
3499
3727
 
3500
3728
  // _apps/@onklave/agent-cli/src/services/work-item-runner.service.ts
3501
3729
  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";
3730
+ import * as fs8 from "fs";
3731
+ import * as os5 from "os";
3732
+ import * as path8 from "path";
3505
3733
  var WorkItemRunner = class {
3506
3734
  constructor(opts = {}) {
3507
3735
  this.sessionManager = opts.sessionManager ?? new SessionManager();
@@ -3522,10 +3750,10 @@ var WorkItemRunner = class {
3522
3750
  summary: `Work item ${workItem.id} has no clonable repo (project.repos[0].url missing).`
3523
3751
  };
3524
3752
  }
3525
- const workDir = fs5.mkdtempSync(
3526
- path6.join(os4.tmpdir(), `onklave-pickup-${sessionId}-`)
3753
+ const workDir = fs8.mkdtempSync(
3754
+ path8.join(os5.tmpdir(), `onklave-pickup-${sessionId}-`)
3527
3755
  );
3528
- const repoDir = path6.join(workDir, sanitizeName(repo.name) || "repo");
3756
+ const repoDir = path8.join(workDir, sanitizeName(repo.name) || "repo");
3529
3757
  const branchName = `onklave/work-item/${workItem.id}`;
3530
3758
  try {
3531
3759
  await this.git(["clone", repo.url, repoDir], workDir);
@@ -3577,7 +3805,7 @@ var WorkItemRunner = class {
3577
3805
  systemPromptAppend: null
3578
3806
  };
3579
3807
  let output = "";
3580
- const exitCode = await new Promise((resolve3) => {
3808
+ const exitCode = await new Promise((resolve4) => {
3581
3809
  const timeout = setTimeout(() => {
3582
3810
  void this.sessionManager.stopSession(sessionId).catch(() => void 0);
3583
3811
  }, this.timeoutSeconds * 1e3);
@@ -3590,7 +3818,7 @@ var WorkItemRunner = class {
3590
3818
  },
3591
3819
  onExit: (code) => {
3592
3820
  clearTimeout(timeout);
3593
- resolve3(code);
3821
+ resolve4(code);
3594
3822
  }
3595
3823
  });
3596
3824
  });
@@ -3623,12 +3851,12 @@ function sanitizeName(name) {
3623
3851
  }
3624
3852
  function cleanup(dir) {
3625
3853
  try {
3626
- fs5.rmSync(dir, { recursive: true, force: true });
3854
+ fs8.rmSync(dir, { recursive: true, force: true });
3627
3855
  } catch {
3628
3856
  }
3629
3857
  }
3630
3858
  function defaultGit(args, cwd) {
3631
- return new Promise((resolve3, reject) => {
3859
+ return new Promise((resolve4, reject) => {
3632
3860
  const child = spawn2("git", args, {
3633
3861
  cwd,
3634
3862
  stdio: ["ignore", "pipe", "pipe"]
@@ -3640,7 +3868,7 @@ function defaultGit(args, cwd) {
3640
3868
  child.on("error", (err) => reject(err));
3641
3869
  child.on("exit", (code) => {
3642
3870
  if (code === 0) {
3643
- resolve3();
3871
+ resolve4();
3644
3872
  } else {
3645
3873
  reject(new Error(`git ${args[0]} exited ${code}: ${stderr.trim()}`));
3646
3874
  }
@@ -3649,7 +3877,7 @@ function defaultGit(args, cwd) {
3649
3877
  }
3650
3878
 
3651
3879
  // _apps/@onklave/agent-cli/src/services/daemon-resource-sampler.service.ts
3652
- import * as os5 from "os";
3880
+ import * as os6 from "os";
3653
3881
  var DEFAULT_SAMPLE_INTERVAL_MS = 5e3;
3654
3882
  var DEFAULT_WINDOW_MS = 6e4;
3655
3883
  var DaemonResourceSampler = class {
@@ -3717,8 +3945,8 @@ var DaemonResourceSampler = class {
3717
3945
  }
3718
3946
  };
3719
3947
  function defaultCpuPercent() {
3720
- const cpus2 = os5.cpus().length || 1;
3721
- const load1m = os5.loadavg()[0];
3948
+ const cpus2 = os6.cpus().length || 1;
3949
+ const load1m = os6.loadavg()[0];
3722
3950
  return load1m / cpus2 * 100;
3723
3951
  }
3724
3952
  function defaultMemoryRss() {
@@ -3749,7 +3977,7 @@ var DaemonSpawner = class {
3749
3977
  orgId: req.orgId,
3750
3978
  systemPromptAppend: null
3751
3979
  };
3752
- return new Promise((resolve3, reject) => {
3980
+ return new Promise((resolve4, reject) => {
3753
3981
  void this.sessionManager.spawnSession(req.sessionId, config, {
3754
3982
  onStdout: (data) => {
3755
3983
  process.stdout.write(data);
@@ -3759,7 +3987,7 @@ var DaemonSpawner = class {
3759
3987
  },
3760
3988
  onExit: (code, signal) => {
3761
3989
  if (code === 0) {
3762
- resolve3();
3990
+ resolve4();
3763
3991
  } else {
3764
3992
  reject(
3765
3993
  new Error(
@@ -3910,123 +4138,48 @@ var DaemonTokenRefresher = class {
3910
4138
  }
3911
4139
  };
3912
4140
 
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 {
4141
+ // _apps/@onklave/agent-cli/src/services/daemon-upgrade.service.ts
4142
+ var DaemonUpgradeService = class {
3917
4143
  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;
4144
+ this.opts = opts;
3924
4145
  }
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() {
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 });
3932
4159
  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)`
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.`
3952
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}`);
3953
4175
  }
3954
4176
  }
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
4177
  };
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
4178
 
4026
4179
  // _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";
4180
+ import * as fs9 from "fs";
4181
+ import * as path9 from "path";
4182
+ import * as os7 from "os";
4030
4183
  var VALID_TRANSITIONS = {
4031
4184
  installing: ["registered"],
4032
4185
  registered: ["starting"],
@@ -4037,10 +4190,10 @@ var VALID_TRANSITIONS = {
4037
4190
  stopped: ["starting"]
4038
4191
  };
4039
4192
  function defaultStateFilePath() {
4040
- return path8.join(os6.homedir(), ".config", "onklave", "daemon.state.json");
4193
+ return path9.join(os7.homedir(), ".config", "onklave", "daemon.state.json");
4041
4194
  }
4042
4195
  function defaultPidFilePath() {
4043
- return path8.join(os6.homedir(), ".config", "onklave", "daemon.pid");
4196
+ return path9.join(os7.homedir(), ".config", "onklave", "daemon.pid");
4044
4197
  }
4045
4198
  var DaemonStateError = class extends Error {
4046
4199
  constructor(message) {
@@ -4064,8 +4217,8 @@ var DaemonStateService = class {
4064
4217
  */
4065
4218
  static readPersisted(stateFile = defaultStateFilePath()) {
4066
4219
  try {
4067
- if (!fs7.existsSync(stateFile)) return null;
4068
- const raw = fs7.readFileSync(stateFile, "utf8");
4220
+ if (!fs9.existsSync(stateFile)) return null;
4221
+ const raw = fs9.readFileSync(stateFile, "utf8");
4069
4222
  const parsed = JSON.parse(raw);
4070
4223
  if (!parsed.state || !parsed.enteredAt) return null;
4071
4224
  return parsed;
@@ -4115,8 +4268,8 @@ var DaemonStateService = class {
4115
4268
  }
4116
4269
  }
4117
4270
  persist(reason) {
4118
- const dir = path8.dirname(this.stateFile);
4119
- fs7.mkdirSync(dir, { recursive: true });
4271
+ const dir = path9.dirname(this.stateFile);
4272
+ fs9.mkdirSync(dir, { recursive: true });
4120
4273
  const payload = {
4121
4274
  state: this.current,
4122
4275
  enteredAt: this.enteredAt.toISOString(),
@@ -4126,8 +4279,8 @@ var DaemonStateService = class {
4126
4279
  ...this.latestRuntime ? { runtime: this.latestRuntime } : {}
4127
4280
  };
4128
4281
  const tmp = `${this.stateFile}.tmp`;
4129
- fs7.writeFileSync(tmp, JSON.stringify(payload, null, 2));
4130
- fs7.renameSync(tmp, this.stateFile);
4282
+ fs9.writeFileSync(tmp, JSON.stringify(payload, null, 2));
4283
+ fs9.renameSync(tmp, this.stateFile);
4131
4284
  }
4132
4285
  /**
4133
4286
  * Publish the latest runtime snapshot. Persists to the state file
@@ -4145,7 +4298,7 @@ var DaemonStateService = class {
4145
4298
  */
4146
4299
  clearPersisted() {
4147
4300
  try {
4148
- fs7.unlinkSync(this.stateFile);
4301
+ fs9.unlinkSync(this.stateFile);
4149
4302
  } catch {
4150
4303
  }
4151
4304
  }
@@ -4389,7 +4542,9 @@ async function daemonStart() {
4389
4542
  platformUrl,
4390
4543
  deviceToken: creds.deviceToken,
4391
4544
  machineId: creds.machineId,
4392
- getActiveSessionCount: () => claimHandler.getActiveSessionCount()
4545
+ getActiveSessionCount: () => claimHandler.getActiveSessionCount(),
4546
+ cliVersion: readPackageVersion() ?? "0.0.0",
4547
+ posture: collectPosture()
4393
4548
  });
4394
4549
  const drainAndExit = async (reason) => {
4395
4550
  if (shuttingDown) return;
@@ -4452,6 +4607,35 @@ async function daemonStart() {
4452
4607
  }
4453
4608
  console.log("Connected.");
4454
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
+ });
4455
4639
  await stateService.transition("online");
4456
4640
  heartbeat.start();
4457
4641
  resourceSampler.start();
@@ -4601,7 +4785,7 @@ function transitionToAction(next) {
4601
4785
  }
4602
4786
  function readPid(pidFile) {
4603
4787
  try {
4604
- const raw = fs8.readFileSync(pidFile, "utf8").trim();
4788
+ const raw = fs10.readFileSync(pidFile, "utf8").trim();
4605
4789
  const parsed = Number.parseInt(raw, 10);
4606
4790
  return Number.isFinite(parsed) && parsed > 0 ? parsed : null;
4607
4791
  } catch {
@@ -4610,12 +4794,12 @@ function readPid(pidFile) {
4610
4794
  }
4611
4795
  function writePid(pidFile) {
4612
4796
  const dir = pidFile.replace(/\/[^/]+$/, "");
4613
- fs8.mkdirSync(dir, { recursive: true });
4614
- fs8.writeFileSync(pidFile, String(process.pid), { mode: 384 });
4797
+ fs10.mkdirSync(dir, { recursive: true });
4798
+ fs10.writeFileSync(pidFile, String(process.pid), { mode: 384 });
4615
4799
  }
4616
4800
  function removePid(pidFile) {
4617
4801
  try {
4618
- fs8.unlinkSync(pidFile);
4802
+ fs10.unlinkSync(pidFile);
4619
4803
  } catch {
4620
4804
  }
4621
4805
  }
@@ -4659,15 +4843,15 @@ var COMMANDS = {
4659
4843
  };
4660
4844
 
4661
4845
  // _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";
4846
+ import * as fs11 from "node:fs";
4847
+ import * as path10 from "node:path";
4848
+ import * as os8 from "node:os";
4665
4849
  import { createInterface } from "node:readline/promises";
4666
4850
  import { spawnSync } from "node:child_process";
4667
4851
  var CHECK_TTL_MS = 24 * 60 * 60 * 1e3;
4668
4852
  var FETCH_DEADLINE_MS = 1500;
4669
- var CACHE_PATH = path9.join(
4670
- os7.homedir(),
4853
+ var CACHE_PATH = path10.join(
4854
+ os8.homedir(),
4671
4855
  ".config",
4672
4856
  "onklave",
4673
4857
  "update-check.json"
@@ -4746,7 +4930,7 @@ async function maybeNotifyUpdate(deps = {}) {
4746
4930
  }
4747
4931
  }
4748
4932
  function withTimeout(promise, ms) {
4749
- return new Promise((resolve3, reject) => {
4933
+ return new Promise((resolve4, reject) => {
4750
4934
  const timer = setTimeout(
4751
4935
  () => reject(new Error("update check timed out")),
4752
4936
  ms
@@ -4755,7 +4939,7 @@ function withTimeout(promise, ms) {
4755
4939
  promise.then(
4756
4940
  (value) => {
4757
4941
  clearTimeout(timer);
4758
- resolve3(value);
4942
+ resolve4(value);
4759
4943
  },
4760
4944
  (err) => {
4761
4945
  clearTimeout(timer);
@@ -4777,16 +4961,16 @@ function isCacheFresh(cache, nowMs) {
4777
4961
  }
4778
4962
  function readCache() {
4779
4963
  try {
4780
- if (!fs9.existsSync(CACHE_PATH)) return {};
4781
- return JSON.parse(fs9.readFileSync(CACHE_PATH, "utf8"));
4964
+ if (!fs11.existsSync(CACHE_PATH)) return {};
4965
+ return JSON.parse(fs11.readFileSync(CACHE_PATH, "utf8"));
4782
4966
  } catch {
4783
4967
  return {};
4784
4968
  }
4785
4969
  }
4786
4970
  function writeCache(cache) {
4787
4971
  try {
4788
- fs9.mkdirSync(path9.dirname(CACHE_PATH), { recursive: true, mode: 448 });
4789
- 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");
4790
4974
  } catch {
4791
4975
  }
4792
4976
  }
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.46",
4
4
  "description": "Onklave Agent CLI — local agent runner with cloud orchestration",
5
5
  "bin": {
6
6
  "onklave": "./main.js"