@onklave/agent-cli 0.1.10 → 0.1.12
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.
- package/main.js +742 -18
- package/package.json +1 -1
package/main.js
CHANGED
|
@@ -510,7 +510,7 @@ var PlatformClient = class {
|
|
|
510
510
|
async respondToGate(sessionId, decision, value) {
|
|
511
511
|
await this.request(
|
|
512
512
|
"POST",
|
|
513
|
-
`/api/v1/agent/sessions/${sessionId}/
|
|
513
|
+
`/api/v1/agent/sessions/${sessionId}/respond`,
|
|
514
514
|
{
|
|
515
515
|
decision,
|
|
516
516
|
message: value
|
|
@@ -569,6 +569,17 @@ var PlatformClient = class {
|
|
|
569
569
|
async unregisterMachine() {
|
|
570
570
|
await this.request("POST", "/api/v1/runner/unregister");
|
|
571
571
|
}
|
|
572
|
+
/*
|
|
573
|
+
* Update an already-registered machine's configuration. Used by
|
|
574
|
+
* `onklave register --refresh` to push freshly-detected capabilities.
|
|
575
|
+
*/
|
|
576
|
+
async updateMachine(machineId, patch) {
|
|
577
|
+
await this.request(
|
|
578
|
+
"PATCH",
|
|
579
|
+
`/api/v1/runner/machines/${machineId}`,
|
|
580
|
+
patch
|
|
581
|
+
);
|
|
582
|
+
}
|
|
572
583
|
/*
|
|
573
584
|
* List all registered machines.
|
|
574
585
|
*/
|
|
@@ -593,8 +604,8 @@ var PlatformClient = class {
|
|
|
593
604
|
/*
|
|
594
605
|
* Make an authenticated HTTP request to the platform.
|
|
595
606
|
*/
|
|
596
|
-
async request(method,
|
|
597
|
-
const url = `${this.baseUrl}${
|
|
607
|
+
async request(method, path7, body) {
|
|
608
|
+
const url = `${this.baseUrl}${path7}`;
|
|
598
609
|
const headers = {
|
|
599
610
|
Authorization: `Bearer ${this.token}`,
|
|
600
611
|
"Content-Type": "application/json",
|
|
@@ -642,12 +653,19 @@ var CommsClient = class {
|
|
|
642
653
|
}
|
|
643
654
|
/*
|
|
644
655
|
* Connect to the Onklave comms service via Socket.IO.
|
|
656
|
+
* Pass `machineId` so the server can route inbound runner events
|
|
657
|
+
* (e.g. `assignment:claim-available`) to this specific runner.
|
|
645
658
|
*/
|
|
646
|
-
async connect(platformUrl, token) {
|
|
659
|
+
async connect(platformUrl, token, extra) {
|
|
647
660
|
return new Promise((resolve3, reject) => {
|
|
648
661
|
const commsUrl = platformUrl.replace(/\/+$/, "");
|
|
649
|
-
this.socket = io(`${commsUrl}/agent`, {
|
|
650
|
-
auth: {
|
|
662
|
+
this.socket = io(`${commsUrl}/agent-cli`, {
|
|
663
|
+
auth: {
|
|
664
|
+
token,
|
|
665
|
+
machineId: extra?.machineId,
|
|
666
|
+
deviceToken: extra?.deviceToken,
|
|
667
|
+
orgId: extra?.orgId
|
|
668
|
+
},
|
|
651
669
|
transports: ["websocket", "polling"],
|
|
652
670
|
reconnection: true,
|
|
653
671
|
reconnectionAttempts: this.maxReconnectAttempts,
|
|
@@ -713,10 +731,13 @@ var CommsClient = class {
|
|
|
713
731
|
this.socket?.emit("agent:audit", event);
|
|
714
732
|
}
|
|
715
733
|
/*
|
|
716
|
-
* Emit a heartbeat to the platform.
|
|
734
|
+
* Emit a heartbeat to the platform. The comms namespace expects the
|
|
735
|
+
* `runner:heartbeat` event and the RunnerHeartbeatPayload shape; the
|
|
736
|
+
* primary heartbeat path is HTTP via HeartbeatService, this socket path
|
|
737
|
+
* is reserved for the future daemon-with-open-socket flow.
|
|
717
738
|
*/
|
|
718
739
|
emitHeartbeat(data) {
|
|
719
|
-
this.socket?.emit("
|
|
740
|
+
this.socket?.emit("runner:heartbeat", data);
|
|
720
741
|
}
|
|
721
742
|
/*
|
|
722
743
|
* Listen for approval responses from the platform.
|
|
@@ -738,6 +759,15 @@ var CommsClient = class {
|
|
|
738
759
|
onSpawnRequest(handler) {
|
|
739
760
|
this.socket?.on("agent:spawn", handler);
|
|
740
761
|
}
|
|
762
|
+
/*
|
|
763
|
+
* Listen for assignment:claim-available events from the platform.
|
|
764
|
+
* The daemon mode (V6-CLI-002, deferred) will consume these to claim
|
|
765
|
+
* work items. For now this stays as a registerable listener so
|
|
766
|
+
* non-daemon use is unaffected.
|
|
767
|
+
*/
|
|
768
|
+
onAssignmentAvailable(handler) {
|
|
769
|
+
this.socket?.on("assignment:claim-available", handler);
|
|
770
|
+
}
|
|
741
771
|
};
|
|
742
772
|
|
|
743
773
|
// _apps/@onklave/agent-cli/src/services/session-manager.ts
|
|
@@ -762,6 +792,12 @@ var SessionManager = class {
|
|
|
762
792
|
if (config.allowedTools && config.allowedTools.length > 0) {
|
|
763
793
|
args.push("--allowedTools", config.allowedTools.join(","));
|
|
764
794
|
}
|
|
795
|
+
if (config.deniedTools && config.deniedTools.length > 0) {
|
|
796
|
+
args.push("--disallowedTools", config.deniedTools.join(","));
|
|
797
|
+
}
|
|
798
|
+
if (config.addDirs && config.addDirs.length > 0) {
|
|
799
|
+
args.push("--add-dir", ...config.addDirs);
|
|
800
|
+
}
|
|
765
801
|
if (config.systemPromptAppend) {
|
|
766
802
|
args.push("--append-system-prompt", config.systemPromptAppend);
|
|
767
803
|
}
|
|
@@ -872,6 +908,20 @@ var SessionManager = class {
|
|
|
872
908
|
}
|
|
873
909
|
};
|
|
874
910
|
|
|
911
|
+
// _apps/@onklave/agent-cli/src/types/events.types.ts
|
|
912
|
+
var DAEMON_AUDIT_ACTIONS = {
|
|
913
|
+
STARTED: "daemon.started",
|
|
914
|
+
ONLINE: "daemon.online",
|
|
915
|
+
DRAINING: "daemon.draining",
|
|
916
|
+
STOPPED: "daemon.stopped",
|
|
917
|
+
FORCE_STOPPED: "daemon.force_stopped",
|
|
918
|
+
DRAIN_TIMEOUT_EXCEEDED: "daemon.drain_timeout_exceeded",
|
|
919
|
+
AUTH_WARNING: "daemon.auth_warning",
|
|
920
|
+
AUTH_EXPIRED: "daemon.auth_expired",
|
|
921
|
+
HEARTBEAT_FAILED: "daemon.heartbeat_failed",
|
|
922
|
+
RESOURCE_LIMIT_BREACHED: "daemon.resource_limit_breached"
|
|
923
|
+
};
|
|
924
|
+
|
|
875
925
|
// _apps/@onklave/agent-cli/src/services/state-publisher.ts
|
|
876
926
|
var StatePublisher = class {
|
|
877
927
|
constructor(commsClient) {
|
|
@@ -1178,6 +1228,69 @@ var GuardrailEnforcer = class {
|
|
|
1178
1228
|
}
|
|
1179
1229
|
};
|
|
1180
1230
|
|
|
1231
|
+
// _apps/@onklave/agent-cli/src/services/heartbeat.service.ts
|
|
1232
|
+
var DEFAULT_INTERVAL_MS = 3e4;
|
|
1233
|
+
var HeartbeatService = class {
|
|
1234
|
+
constructor(opts) {
|
|
1235
|
+
this.opts = opts;
|
|
1236
|
+
this.timer = null;
|
|
1237
|
+
this.intervalMs = opts.intervalMs ?? DEFAULT_INTERVAL_MS;
|
|
1238
|
+
this.onError = opts.onError ?? ((err) => console.error(`[heartbeat] ${err.message}`));
|
|
1239
|
+
}
|
|
1240
|
+
start() {
|
|
1241
|
+
if (this.timer) return;
|
|
1242
|
+
void this.emit();
|
|
1243
|
+
this.timer = setInterval(() => {
|
|
1244
|
+
void this.emit();
|
|
1245
|
+
}, this.intervalMs);
|
|
1246
|
+
if (typeof this.timer.unref === "function") {
|
|
1247
|
+
this.timer.unref();
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
stop() {
|
|
1251
|
+
if (this.timer) {
|
|
1252
|
+
clearInterval(this.timer);
|
|
1253
|
+
this.timer = null;
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
/**
|
|
1257
|
+
* Single heartbeat emission. Exposed for tests and one-shot diagnostics
|
|
1258
|
+
* (`onklave doctor`).
|
|
1259
|
+
*/
|
|
1260
|
+
async emit() {
|
|
1261
|
+
const body = {
|
|
1262
|
+
machineId: this.opts.machineId,
|
|
1263
|
+
status: "online",
|
|
1264
|
+
activeSessionCount: this.opts.getActiveSessionCount(),
|
|
1265
|
+
resourceUsage: this.opts.getResourceUsage?.()
|
|
1266
|
+
};
|
|
1267
|
+
try {
|
|
1268
|
+
const response = await fetch(
|
|
1269
|
+
`${this.opts.platformUrl}/api/v1/runner/heartbeat`,
|
|
1270
|
+
{
|
|
1271
|
+
method: "POST",
|
|
1272
|
+
headers: {
|
|
1273
|
+
"Content-Type": "application/json",
|
|
1274
|
+
Accept: "application/json",
|
|
1275
|
+
"x-device-token": this.opts.deviceToken
|
|
1276
|
+
},
|
|
1277
|
+
body: JSON.stringify(body),
|
|
1278
|
+
signal: AbortSignal.timeout(1e4)
|
|
1279
|
+
}
|
|
1280
|
+
);
|
|
1281
|
+
if (!response.ok) {
|
|
1282
|
+
this.onError(
|
|
1283
|
+
new Error(
|
|
1284
|
+
`heartbeat failed: ${response.status} ${response.statusText}`
|
|
1285
|
+
)
|
|
1286
|
+
);
|
|
1287
|
+
}
|
|
1288
|
+
} catch (err) {
|
|
1289
|
+
this.onError(err instanceof Error ? err : new Error(String(err)));
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
};
|
|
1293
|
+
|
|
1181
1294
|
// _apps/@onklave/agent-cli/src/tui/render.tsx
|
|
1182
1295
|
import { render } from "ink";
|
|
1183
1296
|
|
|
@@ -1764,6 +1877,21 @@ async function runCommand(args) {
|
|
|
1764
1877
|
resolvedConfig.platformUrl,
|
|
1765
1878
|
creds.token
|
|
1766
1879
|
);
|
|
1880
|
+
const sessionManager = new SessionManager();
|
|
1881
|
+
let heartbeat = null;
|
|
1882
|
+
if (creds.machineId && creds.deviceToken) {
|
|
1883
|
+
heartbeat = new HeartbeatService({
|
|
1884
|
+
platformUrl: resolvedConfig.platformUrl,
|
|
1885
|
+
deviceToken: creds.deviceToken,
|
|
1886
|
+
machineId: creds.machineId,
|
|
1887
|
+
getActiveSessionCount: () => sessionManager.getActiveSessionIds().length
|
|
1888
|
+
});
|
|
1889
|
+
heartbeat.start();
|
|
1890
|
+
const stopHeartbeat = () => heartbeat?.stop();
|
|
1891
|
+
process.once("SIGTERM", stopHeartbeat);
|
|
1892
|
+
process.once("SIGINT", stopHeartbeat);
|
|
1893
|
+
process.once("beforeExit", stopHeartbeat);
|
|
1894
|
+
}
|
|
1767
1895
|
let sessionId;
|
|
1768
1896
|
try {
|
|
1769
1897
|
console.log("Creating agent session on Onklave platform...");
|
|
@@ -1788,7 +1916,11 @@ async function runCommand(args) {
|
|
|
1788
1916
|
const auditStreamer = new AuditStreamer(commsClient);
|
|
1789
1917
|
try {
|
|
1790
1918
|
console.log("Connecting to Onklave comms service...");
|
|
1791
|
-
await commsClient.connect(resolvedConfig.platformUrl, creds.token
|
|
1919
|
+
await commsClient.connect(resolvedConfig.platformUrl, creds.token, {
|
|
1920
|
+
machineId: creds.machineId,
|
|
1921
|
+
deviceToken: creds.deviceToken,
|
|
1922
|
+
orgId: creds.orgId
|
|
1923
|
+
});
|
|
1792
1924
|
console.log("Connected.");
|
|
1793
1925
|
} catch (err) {
|
|
1794
1926
|
console.warn(
|
|
@@ -1817,13 +1949,27 @@ async function runCommand(args) {
|
|
|
1817
1949
|
);
|
|
1818
1950
|
}
|
|
1819
1951
|
}
|
|
1820
|
-
const
|
|
1952
|
+
const guardrailEnforcer = new GuardrailEnforcer({
|
|
1821
1953
|
rules: resolvedConfig.guardrails,
|
|
1822
1954
|
allowedTools: resolvedConfig.allowedTools,
|
|
1823
1955
|
deniedTools: resolvedConfig.deniedTools,
|
|
1824
1956
|
readablePaths: resolvedConfig.readablePaths,
|
|
1825
1957
|
writablePaths: resolvedConfig.writablePaths
|
|
1826
1958
|
});
|
|
1959
|
+
for (const tool of resolvedConfig.allowedTools) {
|
|
1960
|
+
const verdict = guardrailEnforcer.checkToolCall(tool, {});
|
|
1961
|
+
if (!verdict.allowed) {
|
|
1962
|
+
console.warn(
|
|
1963
|
+
`Warning: allowed tool "${tool}" is also denied by guardrails \u2014 Claude will reject this tool.`
|
|
1964
|
+
);
|
|
1965
|
+
}
|
|
1966
|
+
}
|
|
1967
|
+
const addDirs = [
|
|
1968
|
+
.../* @__PURE__ */ new Set([
|
|
1969
|
+
...resolvedConfig.writablePaths,
|
|
1970
|
+
...resolvedConfig.readablePaths
|
|
1971
|
+
])
|
|
1972
|
+
];
|
|
1827
1973
|
const sessionConfig = {
|
|
1828
1974
|
task: resolvedConfig.task,
|
|
1829
1975
|
context: resolvedConfig.context,
|
|
@@ -1834,12 +1980,25 @@ async function runCommand(args) {
|
|
|
1834
1980
|
guardrails: resolvedConfig.guardrails,
|
|
1835
1981
|
allowedTools: resolvedConfig.allowedTools,
|
|
1836
1982
|
deniedTools: resolvedConfig.deniedTools,
|
|
1983
|
+
addDirs,
|
|
1837
1984
|
apiKey: resolvedConfig.apiKey,
|
|
1838
1985
|
headless: resolvedConfig.headless,
|
|
1839
1986
|
platformUrl: resolvedConfig.platformUrl,
|
|
1840
1987
|
orgId: resolvedConfig.orgId,
|
|
1841
1988
|
systemPromptAppend: personaSystemPrompt ?? resolvedConfig.systemPromptAppend
|
|
1842
1989
|
};
|
|
1990
|
+
auditStreamer.record({
|
|
1991
|
+
sessionId,
|
|
1992
|
+
type: "state_change" /* STATE_CHANGE */,
|
|
1993
|
+
action: "guardrails_applied",
|
|
1994
|
+
details: {
|
|
1995
|
+
ruleCount: resolvedConfig.guardrails.length,
|
|
1996
|
+
allowedTools: resolvedConfig.allowedTools,
|
|
1997
|
+
deniedTools: resolvedConfig.deniedTools,
|
|
1998
|
+
addDirs
|
|
1999
|
+
},
|
|
2000
|
+
outcome: "success"
|
|
2001
|
+
});
|
|
1843
2002
|
statePublisher.publishSessionStarted(sessionId, sessionConfig);
|
|
1844
2003
|
auditStreamer.record({
|
|
1845
2004
|
sessionId,
|
|
@@ -1849,7 +2008,6 @@ async function runCommand(args) {
|
|
|
1849
2008
|
outcome: "success"
|
|
1850
2009
|
});
|
|
1851
2010
|
const useTui = flags["tui"] === true || process.stdout.isTTY === true && !resolvedConfig.headless && flags["tui"] !== false;
|
|
1852
|
-
const sessionManager = new SessionManager();
|
|
1853
2011
|
if (useTui) {
|
|
1854
2012
|
const tuiInstance = renderTui({
|
|
1855
2013
|
sessionId,
|
|
@@ -2568,11 +2726,14 @@ async function configCommand(args) {
|
|
|
2568
2726
|
import * as os3 from "os";
|
|
2569
2727
|
async function registerCommand(args) {
|
|
2570
2728
|
const { flags } = parseArgs(args);
|
|
2729
|
+
const refresh = flags["refresh"] === true;
|
|
2571
2730
|
const linkingToken = flags["token"];
|
|
2572
|
-
if (typeof linkingToken !== "string" || !linkingToken) {
|
|
2573
|
-
console.error("Error: --token is required.\n");
|
|
2574
|
-
console.log("Usage:
|
|
2575
|
-
console.log("
|
|
2731
|
+
if (!refresh && (typeof linkingToken !== "string" || !linkingToken)) {
|
|
2732
|
+
console.error("Error: --token is required (or pass --refresh).\n");
|
|
2733
|
+
console.log("Usage:");
|
|
2734
|
+
console.log(" onklave register --token <linking-token>");
|
|
2735
|
+
console.log(" onklave register --refresh # re-detect capabilities");
|
|
2736
|
+
console.log("\nTo obtain a linking token:");
|
|
2576
2737
|
console.log(" 1. Log in to the Onklave Portal");
|
|
2577
2738
|
console.log(" 2. Navigate to Settings > Machines");
|
|
2578
2739
|
console.log(' 3. Click "Register Machine" to generate a linking token');
|
|
@@ -2586,6 +2747,28 @@ async function registerCommand(args) {
|
|
|
2586
2747
|
process.exitCode = 1;
|
|
2587
2748
|
return;
|
|
2588
2749
|
}
|
|
2750
|
+
const platformClient = new PlatformClient(creds.platformUrl, creds.token);
|
|
2751
|
+
if (refresh) {
|
|
2752
|
+
if (!creds.machineId) {
|
|
2753
|
+
console.error(
|
|
2754
|
+
"Error: --refresh requires a previously-registered machine. Run `onklave register --token <linking-token>` first."
|
|
2755
|
+
);
|
|
2756
|
+
process.exitCode = 1;
|
|
2757
|
+
return;
|
|
2758
|
+
}
|
|
2759
|
+
const capabilities = detectCapabilities();
|
|
2760
|
+
console.log("Re-detecting capabilities for this machine...");
|
|
2761
|
+
console.log(` Capabilities: ${capabilities.join(", ") || "(none)"}`);
|
|
2762
|
+
try {
|
|
2763
|
+
await platformClient.updateMachine(creds.machineId, { capabilities });
|
|
2764
|
+
console.log(`
|
|
2765
|
+
Machine ${creds.machineId} updated successfully.`);
|
|
2766
|
+
} catch (err) {
|
|
2767
|
+
console.error(`Refresh failed: ${err.message}`);
|
|
2768
|
+
process.exitCode = 1;
|
|
2769
|
+
}
|
|
2770
|
+
return;
|
|
2771
|
+
}
|
|
2589
2772
|
const metadata = {
|
|
2590
2773
|
hostname: os3.hostname(),
|
|
2591
2774
|
os: `${os3.platform()} ${os3.release()}`,
|
|
@@ -2593,14 +2776,19 @@ async function registerCommand(args) {
|
|
|
2593
2776
|
nodeVersion: process.version,
|
|
2594
2777
|
capabilities: detectCapabilities()
|
|
2595
2778
|
};
|
|
2596
|
-
const platformClient = new PlatformClient(creds.platformUrl, creds.token);
|
|
2597
2779
|
try {
|
|
2598
2780
|
console.log("Registering machine with Onklave platform...");
|
|
2599
2781
|
console.log(` Hostname: ${metadata.hostname}`);
|
|
2600
2782
|
console.log(` OS: ${metadata.os}`);
|
|
2601
2783
|
console.log(` Arch: ${metadata.arch}`);
|
|
2602
2784
|
console.log(` Node: ${metadata.nodeVersion}`);
|
|
2603
|
-
|
|
2785
|
+
console.log(
|
|
2786
|
+
` Capabilities: ${metadata.capabilities?.join(", ") || "(none)"}`
|
|
2787
|
+
);
|
|
2788
|
+
const result = await platformClient.registerMachine(
|
|
2789
|
+
linkingToken,
|
|
2790
|
+
metadata
|
|
2791
|
+
);
|
|
2604
2792
|
await authService.saveDeviceToken(result.deviceToken, result.machineId);
|
|
2605
2793
|
console.log(`
|
|
2606
2794
|
Machine registered successfully.`);
|
|
@@ -2610,6 +2798,9 @@ Machine registered successfully.`);
|
|
|
2610
2798
|
`
|
|
2611
2799
|
This machine can now receive remote agent session requests.`
|
|
2612
2800
|
);
|
|
2801
|
+
console.log(
|
|
2802
|
+
`Tip: after installing new tools (e.g. Docker), run \`onklave register --refresh\` to update capabilities.`
|
|
2803
|
+
);
|
|
2613
2804
|
} catch (err) {
|
|
2614
2805
|
console.error(`Registration failed: ${err.message}`);
|
|
2615
2806
|
process.exitCode = 1;
|
|
@@ -2669,6 +2860,538 @@ async function logsCommand(args) {
|
|
|
2669
2860
|
}
|
|
2670
2861
|
}
|
|
2671
2862
|
|
|
2863
|
+
// _apps/@onklave/agent-cli/src/commands/daemon.command.ts
|
|
2864
|
+
import * as fs6 from "fs";
|
|
2865
|
+
|
|
2866
|
+
// _apps/@onklave/agent-cli/src/services/daemon-comms.service.ts
|
|
2867
|
+
var DaemonCommsService = class {
|
|
2868
|
+
constructor(opts, client) {
|
|
2869
|
+
this.disconnectedAt = null;
|
|
2870
|
+
this.stopRequested = false;
|
|
2871
|
+
this.client = client ?? new CommsClient();
|
|
2872
|
+
this.opts = opts;
|
|
2873
|
+
this.sleep = opts.sleep ?? defaultSleep;
|
|
2874
|
+
this.onRetry = opts.onRetry ?? ((attempt, delayMs, err) => console.warn(
|
|
2875
|
+
`[daemon-comms] reconnect attempt ${attempt} after ${delayMs}ms (${err.message})`
|
|
2876
|
+
));
|
|
2877
|
+
}
|
|
2878
|
+
/**
|
|
2879
|
+
* Opens the socket connection. Retries indefinitely with jittered
|
|
2880
|
+
* exponential backoff until either (a) a connect succeeds or (b)
|
|
2881
|
+
* `stop()` is called. Throws only if `stop()` is invoked before any
|
|
2882
|
+
* successful connect.
|
|
2883
|
+
*/
|
|
2884
|
+
async connect() {
|
|
2885
|
+
let attempt = 0;
|
|
2886
|
+
while (!this.stopRequested) {
|
|
2887
|
+
try {
|
|
2888
|
+
await this.client.connect(this.opts.platformUrl, this.opts.token, {
|
|
2889
|
+
machineId: this.opts.machineId,
|
|
2890
|
+
deviceToken: this.opts.deviceToken,
|
|
2891
|
+
orgId: this.opts.orgId
|
|
2892
|
+
});
|
|
2893
|
+
this.disconnectedAt = null;
|
|
2894
|
+
return;
|
|
2895
|
+
} catch (err) {
|
|
2896
|
+
attempt += 1;
|
|
2897
|
+
const delay = this.computeBackoff(attempt);
|
|
2898
|
+
this.onRetry(attempt, delay, err);
|
|
2899
|
+
if (this.disconnectedAt == null) this.disconnectedAt = /* @__PURE__ */ new Date();
|
|
2900
|
+
await this.sleep(delay);
|
|
2901
|
+
}
|
|
2902
|
+
}
|
|
2903
|
+
throw new Error("daemon-comms: stop() called before any connect succeeded");
|
|
2904
|
+
}
|
|
2905
|
+
/**
|
|
2906
|
+
* Signal that no further reconnect should be attempted and close the
|
|
2907
|
+
* underlying socket cleanly. Idempotent.
|
|
2908
|
+
*/
|
|
2909
|
+
disconnect() {
|
|
2910
|
+
this.stopRequested = true;
|
|
2911
|
+
this.client.disconnect();
|
|
2912
|
+
}
|
|
2913
|
+
inner() {
|
|
2914
|
+
return this.client;
|
|
2915
|
+
}
|
|
2916
|
+
isConnected() {
|
|
2917
|
+
return this.client.isConnected();
|
|
2918
|
+
}
|
|
2919
|
+
/**
|
|
2920
|
+
* Continuous-disconnect duration in ms (null if currently connected).
|
|
2921
|
+
* Per spec §3.3, when this exceeds 120s the daemon's `status` output
|
|
2922
|
+
* should report `offline` — the platform will have done so via
|
|
2923
|
+
* heartbeat staleness anyway.
|
|
2924
|
+
*/
|
|
2925
|
+
disconnectedForMs() {
|
|
2926
|
+
if (!this.disconnectedAt) return null;
|
|
2927
|
+
return Date.now() - this.disconnectedAt.getTime();
|
|
2928
|
+
}
|
|
2929
|
+
computeBackoff(attempt) {
|
|
2930
|
+
const max = this.opts.maxBackoffMs ?? 6e4;
|
|
2931
|
+
const base = Math.min(max, 1e3 * 2 ** (attempt - 1));
|
|
2932
|
+
const j = this.opts.jitter ?? 0.2;
|
|
2933
|
+
const swing = base * j;
|
|
2934
|
+
return Math.round(base + (Math.random() * 2 - 1) * swing);
|
|
2935
|
+
}
|
|
2936
|
+
};
|
|
2937
|
+
function defaultSleep(ms) {
|
|
2938
|
+
return new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
2939
|
+
}
|
|
2940
|
+
|
|
2941
|
+
// _apps/@onklave/agent-cli/src/services/daemon-state.service.ts
|
|
2942
|
+
import * as fs5 from "fs";
|
|
2943
|
+
import * as path6 from "path";
|
|
2944
|
+
import * as os4 from "os";
|
|
2945
|
+
var VALID_TRANSITIONS = {
|
|
2946
|
+
installing: ["registered"],
|
|
2947
|
+
registered: ["starting"],
|
|
2948
|
+
starting: ["online", "stopping"],
|
|
2949
|
+
online: ["draining", "stopping"],
|
|
2950
|
+
draining: ["stopping"],
|
|
2951
|
+
stopping: ["stopped"],
|
|
2952
|
+
stopped: ["starting"]
|
|
2953
|
+
};
|
|
2954
|
+
function defaultStateFilePath() {
|
|
2955
|
+
return path6.join(os4.homedir(), ".config", "onklave", "daemon.state.json");
|
|
2956
|
+
}
|
|
2957
|
+
function defaultPidFilePath() {
|
|
2958
|
+
return path6.join(os4.homedir(), ".config", "onklave", "daemon.pid");
|
|
2959
|
+
}
|
|
2960
|
+
var DaemonStateError = class extends Error {
|
|
2961
|
+
constructor(message) {
|
|
2962
|
+
super(message);
|
|
2963
|
+
this.name = "DaemonStateError";
|
|
2964
|
+
}
|
|
2965
|
+
};
|
|
2966
|
+
var DaemonStateService = class {
|
|
2967
|
+
constructor(stateFile = defaultStateFilePath(), initial = "registered", machineId) {
|
|
2968
|
+
this.stateFile = stateFile;
|
|
2969
|
+
this.machineId = machineId;
|
|
2970
|
+
this.listeners = [];
|
|
2971
|
+
this.current = initial;
|
|
2972
|
+
this.enteredAt = /* @__PURE__ */ new Date();
|
|
2973
|
+
}
|
|
2974
|
+
/**
|
|
2975
|
+
* Read the persisted state from disk, if any. Used by `daemon status`
|
|
2976
|
+
* when the daemon process is not running. Returns null on missing or
|
|
2977
|
+
* malformed file rather than throwing so the caller can decide.
|
|
2978
|
+
*/
|
|
2979
|
+
static readPersisted(stateFile = defaultStateFilePath()) {
|
|
2980
|
+
try {
|
|
2981
|
+
if (!fs5.existsSync(stateFile)) return null;
|
|
2982
|
+
const raw = fs5.readFileSync(stateFile, "utf8");
|
|
2983
|
+
const parsed = JSON.parse(raw);
|
|
2984
|
+
if (!parsed.state || !parsed.enteredAt) return null;
|
|
2985
|
+
return parsed;
|
|
2986
|
+
} catch {
|
|
2987
|
+
return null;
|
|
2988
|
+
}
|
|
2989
|
+
}
|
|
2990
|
+
getCurrent() {
|
|
2991
|
+
return this.current;
|
|
2992
|
+
}
|
|
2993
|
+
getEnteredAt() {
|
|
2994
|
+
return this.enteredAt;
|
|
2995
|
+
}
|
|
2996
|
+
/**
|
|
2997
|
+
* Subscribe to transitions. Listeners are awaited sequentially in
|
|
2998
|
+
* registration order. If a listener throws, the transition is treated
|
|
2999
|
+
* as failed — the state is rolled back and the error propagates so the
|
|
3000
|
+
* caller can decide. (Audit emission listeners that fail must therefore
|
|
3001
|
+
* fail the transition — that is the constitutional `audit-fails-action`
|
|
3002
|
+
* coupling described in spec §1.4.)
|
|
3003
|
+
*/
|
|
3004
|
+
onTransition(cb) {
|
|
3005
|
+
this.listeners.push(cb);
|
|
3006
|
+
}
|
|
3007
|
+
/**
|
|
3008
|
+
* Attempt a transition. Throws DaemonStateError on illegal transition
|
|
3009
|
+
* (current → next not in VALID_TRANSITIONS). Throws whatever a listener
|
|
3010
|
+
* throws if any listener fails; in that case state is rolled back to
|
|
3011
|
+
* `prev` and is not persisted.
|
|
3012
|
+
*/
|
|
3013
|
+
async transition(next, opts = {}) {
|
|
3014
|
+
const prev = this.current;
|
|
3015
|
+
const allowed = VALID_TRANSITIONS[prev];
|
|
3016
|
+
if (!allowed.includes(next)) {
|
|
3017
|
+
throw new DaemonStateError(`illegal daemon transition ${prev} \u2192 ${next}`);
|
|
3018
|
+
}
|
|
3019
|
+
this.current = next;
|
|
3020
|
+
this.enteredAt = /* @__PURE__ */ new Date();
|
|
3021
|
+
try {
|
|
3022
|
+
for (const listener of this.listeners) {
|
|
3023
|
+
await listener(next, prev, opts.reason);
|
|
3024
|
+
}
|
|
3025
|
+
this.persist(opts.reason);
|
|
3026
|
+
} catch (err) {
|
|
3027
|
+
this.current = prev;
|
|
3028
|
+
throw err;
|
|
3029
|
+
}
|
|
3030
|
+
}
|
|
3031
|
+
persist(reason) {
|
|
3032
|
+
const dir = path6.dirname(this.stateFile);
|
|
3033
|
+
fs5.mkdirSync(dir, { recursive: true });
|
|
3034
|
+
const payload = {
|
|
3035
|
+
state: this.current,
|
|
3036
|
+
enteredAt: this.enteredAt.toISOString(),
|
|
3037
|
+
pid: process.pid,
|
|
3038
|
+
...this.machineId ? { machineId: this.machineId } : {},
|
|
3039
|
+
...reason ? { reason } : {}
|
|
3040
|
+
};
|
|
3041
|
+
const tmp = `${this.stateFile}.tmp`;
|
|
3042
|
+
fs5.writeFileSync(tmp, JSON.stringify(payload, null, 2));
|
|
3043
|
+
fs5.renameSync(tmp, this.stateFile);
|
|
3044
|
+
}
|
|
3045
|
+
/**
|
|
3046
|
+
* Remove the persisted state file. Called on terminal `stopped` once
|
|
3047
|
+
* the daemon process is exiting cleanly so `daemon status` reports
|
|
3048
|
+
* "registered — daemon not running" instead of stale "stopped".
|
|
3049
|
+
*/
|
|
3050
|
+
clearPersisted() {
|
|
3051
|
+
try {
|
|
3052
|
+
fs5.unlinkSync(this.stateFile);
|
|
3053
|
+
} catch {
|
|
3054
|
+
}
|
|
3055
|
+
}
|
|
3056
|
+
};
|
|
3057
|
+
|
|
3058
|
+
// _apps/@onklave/agent-cli/src/services/unit-file-emitters.ts
|
|
3059
|
+
var SYSTEMD_UNIT = `[Unit]
|
|
3060
|
+
Description=Onklave Agent CLI Daemon
|
|
3061
|
+
Documentation=https://docs.onklave.app/cli/daemon
|
|
3062
|
+
After=network-online.target
|
|
3063
|
+
Wants=network-online.target
|
|
3064
|
+
|
|
3065
|
+
[Service]
|
|
3066
|
+
Type=simple
|
|
3067
|
+
User=onklave
|
|
3068
|
+
Group=onklave
|
|
3069
|
+
Environment="NODE_ENV=production"
|
|
3070
|
+
Environment="ONKLAVE_CONFIG_DIR=/var/lib/onklave"
|
|
3071
|
+
Environment="ONKLAVE_LOG_DIR=/var/log/onklave"
|
|
3072
|
+
ExecStart=/usr/local/bin/onklave daemon
|
|
3073
|
+
ExecStop=/usr/local/bin/onklave daemon drain --timeout 1800
|
|
3074
|
+
Restart=on-failure
|
|
3075
|
+
RestartSec=10s
|
|
3076
|
+
StartLimitIntervalSec=300
|
|
3077
|
+
StartLimitBurst=5
|
|
3078
|
+
KillMode=mixed
|
|
3079
|
+
KillSignal=SIGTERM
|
|
3080
|
+
TimeoutStopSec=1830s
|
|
3081
|
+
|
|
3082
|
+
# Hardening
|
|
3083
|
+
NoNewPrivileges=true
|
|
3084
|
+
ProtectSystem=strict
|
|
3085
|
+
ProtectHome=true
|
|
3086
|
+
PrivateTmp=true
|
|
3087
|
+
ReadWritePaths=/var/lib/onklave /var/log/onklave
|
|
3088
|
+
|
|
3089
|
+
[Install]
|
|
3090
|
+
WantedBy=multi-user.target
|
|
3091
|
+
`;
|
|
3092
|
+
var LAUNCHD_PLIST = `<?xml version="1.0" encoding="UTF-8"?>
|
|
3093
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
3094
|
+
<plist version="1.0">
|
|
3095
|
+
<dict>
|
|
3096
|
+
<key>Label</key>
|
|
3097
|
+
<string>app.onklave.daemon</string>
|
|
3098
|
+
<key>ProgramArguments</key>
|
|
3099
|
+
<array>
|
|
3100
|
+
<string>/usr/local/bin/onklave</string>
|
|
3101
|
+
<string>daemon</string>
|
|
3102
|
+
</array>
|
|
3103
|
+
<key>RunAtLoad</key>
|
|
3104
|
+
<true/>
|
|
3105
|
+
<key>KeepAlive</key>
|
|
3106
|
+
<dict>
|
|
3107
|
+
<key>SuccessfulExit</key>
|
|
3108
|
+
<false/>
|
|
3109
|
+
<key>Crashed</key>
|
|
3110
|
+
<true/>
|
|
3111
|
+
</dict>
|
|
3112
|
+
<key>ThrottleInterval</key>
|
|
3113
|
+
<integer>10</integer>
|
|
3114
|
+
<key>EnvironmentVariables</key>
|
|
3115
|
+
<dict>
|
|
3116
|
+
<key>NODE_ENV</key>
|
|
3117
|
+
<string>production</string>
|
|
3118
|
+
<key>ONKLAVE_CONFIG_DIR</key>
|
|
3119
|
+
<string>/Users/Shared/onklave</string>
|
|
3120
|
+
</dict>
|
|
3121
|
+
<key>StandardOutPath</key>
|
|
3122
|
+
<string>/Users/Shared/onklave/logs/daemon.out.log</string>
|
|
3123
|
+
<key>StandardErrorPath</key>
|
|
3124
|
+
<string>/Users/Shared/onklave/logs/daemon.err.log</string>
|
|
3125
|
+
<key>ProcessType</key>
|
|
3126
|
+
<string>Background</string>
|
|
3127
|
+
</dict>
|
|
3128
|
+
</plist>
|
|
3129
|
+
`;
|
|
3130
|
+
var NSSM_SCRIPT = `@echo off
|
|
3131
|
+
REM Onklave Agent CLI \u2014 NSSM service install script
|
|
3132
|
+
REM Run this as Administrator.
|
|
3133
|
+
|
|
3134
|
+
set NSSM=nssm.exe
|
|
3135
|
+
set SERVICE=OnklaveDaemon
|
|
3136
|
+
set NODE_EXE=%ProgramFiles%\\nodejs\\node.exe
|
|
3137
|
+
set ONKLAVE_CLI=%AppData%\\npm\\node_modules\\@onklave\\agent-cli\\dist\\bin\\onklave.js
|
|
3138
|
+
|
|
3139
|
+
%NSSM% install %SERVICE% "%NODE_EXE%" "%ONKLAVE_CLI%" daemon
|
|
3140
|
+
%NSSM% set %SERVICE% DisplayName "Onklave Agent CLI Daemon"
|
|
3141
|
+
%NSSM% set %SERVICE% Description "Long-running agent worker for the Onklave platform"
|
|
3142
|
+
%NSSM% set %SERVICE% Start SERVICE_AUTO_START
|
|
3143
|
+
%NSSM% set %SERVICE% AppEnvironmentExtra NODE_ENV=production ONKLAVE_CONFIG_DIR=%ProgramData%\\Onklave
|
|
3144
|
+
%NSSM% set %SERVICE% AppStdout %ProgramData%\\Onklave\\logs\\daemon.out.log
|
|
3145
|
+
%NSSM% set %SERVICE% AppStderr %ProgramData%\\Onklave\\logs\\daemon.err.log
|
|
3146
|
+
%NSSM% set %SERVICE% AppRotateFiles 1
|
|
3147
|
+
%NSSM% set %SERVICE% AppRotateOnline 1
|
|
3148
|
+
%NSSM% set %SERVICE% AppRotateBytes 10485760
|
|
3149
|
+
%NSSM% set %SERVICE% AppExit Default Restart
|
|
3150
|
+
%NSSM% set %SERVICE% AppRestartDelay 10000
|
|
3151
|
+
|
|
3152
|
+
%NSSM% start %SERVICE%
|
|
3153
|
+
`;
|
|
3154
|
+
function emitUnitFile(flavour) {
|
|
3155
|
+
switch (flavour) {
|
|
3156
|
+
case "systemd":
|
|
3157
|
+
return SYSTEMD_UNIT;
|
|
3158
|
+
case "launchd":
|
|
3159
|
+
return LAUNCHD_PLIST;
|
|
3160
|
+
case "nssm":
|
|
3161
|
+
return NSSM_SCRIPT;
|
|
3162
|
+
}
|
|
3163
|
+
}
|
|
3164
|
+
|
|
3165
|
+
// _apps/@onklave/agent-cli/src/commands/daemon.command.ts
|
|
3166
|
+
var UNIT_FLAGS = {
|
|
3167
|
+
"--emit-systemd-unit": "systemd",
|
|
3168
|
+
"--emit-launchd-plist": "launchd",
|
|
3169
|
+
"--emit-nssm-script": "nssm"
|
|
3170
|
+
};
|
|
3171
|
+
async function daemonCommand(args) {
|
|
3172
|
+
for (const arg of args) {
|
|
3173
|
+
const flavour = UNIT_FLAGS[arg];
|
|
3174
|
+
if (flavour) {
|
|
3175
|
+
process.stdout.write(emitUnitFile(flavour));
|
|
3176
|
+
return;
|
|
3177
|
+
}
|
|
3178
|
+
}
|
|
3179
|
+
const [sub] = args;
|
|
3180
|
+
if (sub === "status") return daemonStatus();
|
|
3181
|
+
if (sub === "stop" || sub === "drain") return daemonStop();
|
|
3182
|
+
if (sub && !sub.startsWith("--")) {
|
|
3183
|
+
console.error(`Unknown subcommand: ${sub}`);
|
|
3184
|
+
console.error(
|
|
3185
|
+
"Usage: onklave daemon [status|stop|drain] | --emit-systemd-unit | --emit-launchd-plist | --emit-nssm-script"
|
|
3186
|
+
);
|
|
3187
|
+
process.exitCode = 1;
|
|
3188
|
+
return;
|
|
3189
|
+
}
|
|
3190
|
+
return daemonStart();
|
|
3191
|
+
}
|
|
3192
|
+
async function daemonStart() {
|
|
3193
|
+
const authService = new AuthService();
|
|
3194
|
+
const creds = await authService.getCredentials();
|
|
3195
|
+
if (!creds?.deviceToken || !creds?.machineId) {
|
|
3196
|
+
console.error(
|
|
3197
|
+
"Error: machine is not registered. Run: onklave register --token <bootstrap>"
|
|
3198
|
+
);
|
|
3199
|
+
process.exitCode = 1;
|
|
3200
|
+
return;
|
|
3201
|
+
}
|
|
3202
|
+
const pidFile = defaultPidFilePath();
|
|
3203
|
+
if (isPidAlive(pidFile)) {
|
|
3204
|
+
console.error(`Daemon already running (pid ${readPid(pidFile)}).`);
|
|
3205
|
+
process.exitCode = 1;
|
|
3206
|
+
return;
|
|
3207
|
+
}
|
|
3208
|
+
const stateService = new DaemonStateService(
|
|
3209
|
+
defaultStateFilePath(),
|
|
3210
|
+
"registered",
|
|
3211
|
+
creds.machineId
|
|
3212
|
+
);
|
|
3213
|
+
const platformUrl = await authService.getPlatformUrl();
|
|
3214
|
+
const comms = new DaemonCommsService({
|
|
3215
|
+
platformUrl,
|
|
3216
|
+
token: creds.token,
|
|
3217
|
+
machineId: creds.machineId,
|
|
3218
|
+
deviceToken: creds.deviceToken,
|
|
3219
|
+
orgId: creds.orgId
|
|
3220
|
+
});
|
|
3221
|
+
const auditStreamer = new AuditStreamer(comms.inner());
|
|
3222
|
+
stateService.onTransition(async (next, prev, reason) => {
|
|
3223
|
+
const action = transitionToAction(next);
|
|
3224
|
+
if (!action) return;
|
|
3225
|
+
const event = {
|
|
3226
|
+
sessionId: creds.machineId,
|
|
3227
|
+
type: "daemon_lifecycle" /* DAEMON_LIFECYCLE */,
|
|
3228
|
+
action,
|
|
3229
|
+
details: { from: prev, to: next, ...reason ? { reason } : {} },
|
|
3230
|
+
outcome: next === "stopped" && reason ? "failure" : "success"
|
|
3231
|
+
};
|
|
3232
|
+
auditStreamer.record(event);
|
|
3233
|
+
});
|
|
3234
|
+
let activeSessions = 0;
|
|
3235
|
+
let shuttingDown = false;
|
|
3236
|
+
let sigintCount = 0;
|
|
3237
|
+
let sigintTimer = null;
|
|
3238
|
+
const heartbeat = new HeartbeatService({
|
|
3239
|
+
platformUrl,
|
|
3240
|
+
deviceToken: creds.deviceToken,
|
|
3241
|
+
machineId: creds.machineId,
|
|
3242
|
+
getActiveSessionCount: () => activeSessions
|
|
3243
|
+
});
|
|
3244
|
+
const drainAndExit = async (reason) => {
|
|
3245
|
+
if (shuttingDown) return;
|
|
3246
|
+
shuttingDown = true;
|
|
3247
|
+
heartbeat.stop();
|
|
3248
|
+
try {
|
|
3249
|
+
if (stateService.getCurrent() === "online") {
|
|
3250
|
+
await stateService.transition("draining", { reason });
|
|
3251
|
+
}
|
|
3252
|
+
while (activeSessions > 0) {
|
|
3253
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
3254
|
+
}
|
|
3255
|
+
await stateService.transition("stopping", { reason });
|
|
3256
|
+
auditStreamer.flush();
|
|
3257
|
+
comms.disconnect();
|
|
3258
|
+
await stateService.transition("stopped");
|
|
3259
|
+
} finally {
|
|
3260
|
+
removePid(pidFile);
|
|
3261
|
+
stateService.clearPersisted();
|
|
3262
|
+
process.exit(0);
|
|
3263
|
+
}
|
|
3264
|
+
};
|
|
3265
|
+
process.on("SIGTERM", () => {
|
|
3266
|
+
void drainAndExit("SIGTERM");
|
|
3267
|
+
});
|
|
3268
|
+
process.on("SIGINT", () => {
|
|
3269
|
+
sigintCount += 1;
|
|
3270
|
+
if (sigintCount >= 2) {
|
|
3271
|
+
console.error("\nForce-stop requested. Exiting immediately.");
|
|
3272
|
+
removePid(pidFile);
|
|
3273
|
+
process.exit(1);
|
|
3274
|
+
}
|
|
3275
|
+
if (!sigintTimer) {
|
|
3276
|
+
sigintTimer = setTimeout(() => {
|
|
3277
|
+
sigintCount = 0;
|
|
3278
|
+
sigintTimer = null;
|
|
3279
|
+
}, 5e3);
|
|
3280
|
+
sigintTimer.unref?.();
|
|
3281
|
+
}
|
|
3282
|
+
console.log(
|
|
3283
|
+
"\nSIGINT received. Draining (Ctrl-C again within 5s to force stop)\u2026"
|
|
3284
|
+
);
|
|
3285
|
+
void drainAndExit("SIGINT");
|
|
3286
|
+
});
|
|
3287
|
+
await stateService.transition("starting");
|
|
3288
|
+
writePid(pidFile);
|
|
3289
|
+
console.log("Connecting to Onklave comms service...");
|
|
3290
|
+
try {
|
|
3291
|
+
await comms.connect();
|
|
3292
|
+
} catch (err) {
|
|
3293
|
+
console.error(
|
|
3294
|
+
`Comms connect aborted: ${err.message}. Shutting down.`
|
|
3295
|
+
);
|
|
3296
|
+
removePid(pidFile);
|
|
3297
|
+
stateService.clearPersisted();
|
|
3298
|
+
process.exit(1);
|
|
3299
|
+
}
|
|
3300
|
+
console.log("Connected.");
|
|
3301
|
+
await stateService.transition("online");
|
|
3302
|
+
heartbeat.start();
|
|
3303
|
+
console.log(
|
|
3304
|
+
`Daemon online. machineId=${creds.machineId} pid=${process.pid}. Press Ctrl-C to drain.`
|
|
3305
|
+
);
|
|
3306
|
+
await new Promise(() => void 0);
|
|
3307
|
+
}
|
|
3308
|
+
async function daemonStatus() {
|
|
3309
|
+
const persisted = DaemonStateService.readPersisted();
|
|
3310
|
+
if (!persisted) {
|
|
3311
|
+
console.log("registered \u2014 daemon not running");
|
|
3312
|
+
return;
|
|
3313
|
+
}
|
|
3314
|
+
const alive = persisted.pid ? isProcessAlive(persisted.pid) : false;
|
|
3315
|
+
if (!alive) {
|
|
3316
|
+
console.log(
|
|
3317
|
+
`${persisted.state} \u2014 daemon process not running (last entered ${persisted.enteredAt})`
|
|
3318
|
+
);
|
|
3319
|
+
return;
|
|
3320
|
+
}
|
|
3321
|
+
console.log(`State: ${persisted.state}`);
|
|
3322
|
+
if (persisted.machineId)
|
|
3323
|
+
console.log(`Machine ID: ${persisted.machineId}`);
|
|
3324
|
+
if (persisted.pid) console.log(`PID: ${persisted.pid}`);
|
|
3325
|
+
console.log(`Entered state: ${persisted.enteredAt}`);
|
|
3326
|
+
if (persisted.reason) console.log(`Reason: ${persisted.reason}`);
|
|
3327
|
+
}
|
|
3328
|
+
async function daemonStop() {
|
|
3329
|
+
const pidFile = defaultPidFilePath();
|
|
3330
|
+
const pid = readPid(pidFile);
|
|
3331
|
+
if (!pid || !isProcessAlive(pid)) {
|
|
3332
|
+
console.log("No daemon running.");
|
|
3333
|
+
removePid(pidFile);
|
|
3334
|
+
return;
|
|
3335
|
+
}
|
|
3336
|
+
try {
|
|
3337
|
+
process.kill(pid, "SIGTERM");
|
|
3338
|
+
console.log(`Sent SIGTERM to daemon (pid ${pid}).`);
|
|
3339
|
+
} catch (err) {
|
|
3340
|
+
console.error(`Failed to signal daemon: ${err.message}`);
|
|
3341
|
+
process.exitCode = 1;
|
|
3342
|
+
}
|
|
3343
|
+
}
|
|
3344
|
+
function transitionToAction(next) {
|
|
3345
|
+
switch (next) {
|
|
3346
|
+
case "starting":
|
|
3347
|
+
return DAEMON_AUDIT_ACTIONS.STARTED;
|
|
3348
|
+
case "online":
|
|
3349
|
+
return DAEMON_AUDIT_ACTIONS.ONLINE;
|
|
3350
|
+
case "draining":
|
|
3351
|
+
return DAEMON_AUDIT_ACTIONS.DRAINING;
|
|
3352
|
+
case "stopped":
|
|
3353
|
+
return DAEMON_AUDIT_ACTIONS.STOPPED;
|
|
3354
|
+
default:
|
|
3355
|
+
return null;
|
|
3356
|
+
}
|
|
3357
|
+
}
|
|
3358
|
+
function readPid(pidFile) {
|
|
3359
|
+
try {
|
|
3360
|
+
const raw = fs6.readFileSync(pidFile, "utf8").trim();
|
|
3361
|
+
const parsed = Number.parseInt(raw, 10);
|
|
3362
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : null;
|
|
3363
|
+
} catch {
|
|
3364
|
+
return null;
|
|
3365
|
+
}
|
|
3366
|
+
}
|
|
3367
|
+
function writePid(pidFile) {
|
|
3368
|
+
const dir = pidFile.replace(/\/[^/]+$/, "");
|
|
3369
|
+
fs6.mkdirSync(dir, { recursive: true });
|
|
3370
|
+
fs6.writeFileSync(pidFile, String(process.pid), { mode: 384 });
|
|
3371
|
+
}
|
|
3372
|
+
function removePid(pidFile) {
|
|
3373
|
+
try {
|
|
3374
|
+
fs6.unlinkSync(pidFile);
|
|
3375
|
+
} catch {
|
|
3376
|
+
}
|
|
3377
|
+
}
|
|
3378
|
+
function isPidAlive(pidFile) {
|
|
3379
|
+
const pid = readPid(pidFile);
|
|
3380
|
+
if (pid == null) return false;
|
|
3381
|
+
return isProcessAlive(pid);
|
|
3382
|
+
}
|
|
3383
|
+
function isProcessAlive(pid) {
|
|
3384
|
+
try {
|
|
3385
|
+
process.kill(pid, 0);
|
|
3386
|
+
return true;
|
|
3387
|
+
} catch (err) {
|
|
3388
|
+
if (err.code === "EPERM") {
|
|
3389
|
+
return true;
|
|
3390
|
+
}
|
|
3391
|
+
return false;
|
|
3392
|
+
}
|
|
3393
|
+
}
|
|
3394
|
+
|
|
2672
3395
|
// _apps/@onklave/agent-cli/src/commands/index.ts
|
|
2673
3396
|
var COMMANDS = {
|
|
2674
3397
|
login: loginCommand,
|
|
@@ -2687,7 +3410,8 @@ var COMMANDS = {
|
|
|
2687
3410
|
init: (_args) => initCommand(),
|
|
2688
3411
|
config: configCommand,
|
|
2689
3412
|
register: registerCommand,
|
|
2690
|
-
logs: logsCommand
|
|
3413
|
+
logs: logsCommand,
|
|
3414
|
+
daemon: daemonCommand
|
|
2691
3415
|
};
|
|
2692
3416
|
|
|
2693
3417
|
// _apps/@onklave/agent-cli/src/main.ts
|