@onklave/agent-cli 0.1.12 → 0.1.13
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 +813 -19
- package/package.json +1 -1
package/main.js
CHANGED
|
@@ -96,7 +96,7 @@ var AuthService = class {
|
|
|
96
96
|
/*
|
|
97
97
|
* Save the device token received during machine registration.
|
|
98
98
|
*/
|
|
99
|
-
async saveDeviceToken(deviceToken, machineId) {
|
|
99
|
+
async saveDeviceToken(deviceToken, machineId, deviceTokenExpiresAt) {
|
|
100
100
|
this.ensureConfigDir();
|
|
101
101
|
const existing = await this.getCredentials();
|
|
102
102
|
if (!existing) {
|
|
@@ -107,7 +107,8 @@ var AuthService = class {
|
|
|
107
107
|
const credentials = {
|
|
108
108
|
...existing,
|
|
109
109
|
deviceToken,
|
|
110
|
-
machineId
|
|
110
|
+
machineId,
|
|
111
|
+
...deviceTokenExpiresAt ? { deviceTokenExpiresAt } : {}
|
|
111
112
|
};
|
|
112
113
|
fs.writeFileSync(
|
|
113
114
|
this.credentialsPath,
|
|
@@ -554,13 +555,41 @@ var PlatformClient = class {
|
|
|
554
555
|
* Register this machine as an agent runner.
|
|
555
556
|
*/
|
|
556
557
|
async registerMachine(linkingToken, metadata) {
|
|
558
|
+
return this.request("POST", "/api/v1/runner/register", {
|
|
559
|
+
linkingToken,
|
|
560
|
+
metadata
|
|
561
|
+
});
|
|
562
|
+
}
|
|
563
|
+
/*
|
|
564
|
+
* V6-CLI-002 Phase 6 — refresh the device token without re-running
|
|
565
|
+
* the linking-token flow. Caller authenticates with the current
|
|
566
|
+
* device token; backend issues a fresh one with extended expiry.
|
|
567
|
+
*/
|
|
568
|
+
async refreshDeviceToken(currentDeviceToken) {
|
|
569
|
+
return this.request("POST", "/api/v1/runner/register/refresh", void 0, {
|
|
570
|
+
"x-device-token": currentDeviceToken
|
|
571
|
+
});
|
|
572
|
+
}
|
|
573
|
+
/*
|
|
574
|
+
* V6-CLI-002 Phase 5b — claim an assignment as the registered runner.
|
|
575
|
+
*/
|
|
576
|
+
async claimAssignment(assignmentId, machineId, deviceToken) {
|
|
557
577
|
return this.request(
|
|
558
578
|
"POST",
|
|
559
|
-
|
|
560
|
-
{
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
579
|
+
`/api/v1/runner/assignments/${assignmentId}/claim`,
|
|
580
|
+
{ machineId },
|
|
581
|
+
{ "x-device-token": deviceToken }
|
|
582
|
+
);
|
|
583
|
+
}
|
|
584
|
+
/*
|
|
585
|
+
* V6-CLI-002 Phase 5c — release a previously claimed assignment.
|
|
586
|
+
*/
|
|
587
|
+
async releaseAssignment(assignmentId, machineId, reason, deviceToken) {
|
|
588
|
+
return this.request(
|
|
589
|
+
"POST",
|
|
590
|
+
`/api/v1/runner/assignments/${assignmentId}/release`,
|
|
591
|
+
{ machineId, reason },
|
|
592
|
+
{ "x-device-token": deviceToken }
|
|
564
593
|
);
|
|
565
594
|
}
|
|
566
595
|
/*
|
|
@@ -604,12 +633,13 @@ var PlatformClient = class {
|
|
|
604
633
|
/*
|
|
605
634
|
* Make an authenticated HTTP request to the platform.
|
|
606
635
|
*/
|
|
607
|
-
async request(method, path7, body) {
|
|
636
|
+
async request(method, path7, body, extraHeaders) {
|
|
608
637
|
const url = `${this.baseUrl}${path7}`;
|
|
609
638
|
const headers = {
|
|
610
639
|
Authorization: `Bearer ${this.token}`,
|
|
611
640
|
"Content-Type": "application/json",
|
|
612
|
-
Accept: "application/json"
|
|
641
|
+
Accept: "application/json",
|
|
642
|
+
...extraHeaders ?? {}
|
|
613
643
|
};
|
|
614
644
|
const options = {
|
|
615
645
|
method,
|
|
@@ -919,7 +949,13 @@ var DAEMON_AUDIT_ACTIONS = {
|
|
|
919
949
|
AUTH_WARNING: "daemon.auth_warning",
|
|
920
950
|
AUTH_EXPIRED: "daemon.auth_expired",
|
|
921
951
|
HEARTBEAT_FAILED: "daemon.heartbeat_failed",
|
|
922
|
-
RESOURCE_LIMIT_BREACHED: "daemon.resource_limit_breached"
|
|
952
|
+
RESOURCE_LIMIT_BREACHED: "daemon.resource_limit_breached",
|
|
953
|
+
SESSION_CLAIMED: "daemon.session_claimed",
|
|
954
|
+
SESSION_COMPLETED: "daemon.session_completed",
|
|
955
|
+
SESSION_FAILED: "daemon.session_failed",
|
|
956
|
+
UPDATE_AVAILABLE: "daemon.update_available",
|
|
957
|
+
UPDATE_APPLIED: "daemon.update_applied",
|
|
958
|
+
UPDATE_FAILED: "daemon.update_failed"
|
|
923
959
|
};
|
|
924
960
|
|
|
925
961
|
// _apps/@onklave/agent-cli/src/services/state-publisher.ts
|
|
@@ -2789,7 +2825,11 @@ Machine ${creds.machineId} updated successfully.`);
|
|
|
2789
2825
|
linkingToken,
|
|
2790
2826
|
metadata
|
|
2791
2827
|
);
|
|
2792
|
-
await authService.saveDeviceToken(
|
|
2828
|
+
await authService.saveDeviceToken(
|
|
2829
|
+
result.deviceToken,
|
|
2830
|
+
result.machineId,
|
|
2831
|
+
result.deviceTokenExpiresAt
|
|
2832
|
+
);
|
|
2793
2833
|
console.log(`
|
|
2794
2834
|
Machine registered successfully.`);
|
|
2795
2835
|
console.log(` Machine ID: ${result.machineId}`);
|
|
@@ -2938,10 +2978,591 @@ function defaultSleep(ms) {
|
|
|
2938
2978
|
return new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
2939
2979
|
}
|
|
2940
2980
|
|
|
2981
|
+
// _apps/@onklave/agent-cli/src/services/daemon-claim-handler.service.ts
|
|
2982
|
+
var DaemonClaimHandler = class {
|
|
2983
|
+
constructor(opts) {
|
|
2984
|
+
this.activeSessions = 0;
|
|
2985
|
+
this.machineId = opts.machineId;
|
|
2986
|
+
this.audit = opts.audit;
|
|
2987
|
+
this.maxConcurrentSessions = opts.maxConcurrentSessions ?? 2;
|
|
2988
|
+
this.spawnRunner = opts.spawnRunner ?? defaultSpawnStub;
|
|
2989
|
+
this.platformClient = opts.platformClient;
|
|
2990
|
+
this.deviceToken = opts.deviceToken;
|
|
2991
|
+
this.resourceSampler = opts.resourceSampler;
|
|
2992
|
+
this.maxCpuPercent = opts.maxCpuPercent ?? 80;
|
|
2993
|
+
this.maxMemoryBytes = opts.maxMemoryBytes;
|
|
2994
|
+
}
|
|
2995
|
+
/**
|
|
2996
|
+
* Wire up the inbound event subscriptions. Call after the socket has
|
|
2997
|
+
* connected — CommsClient `onSpawnRequest` / `onAssignmentAvailable`
|
|
2998
|
+
* register handlers via `this.socket?.on(...)` which is a no-op when
|
|
2999
|
+
* the socket is null.
|
|
3000
|
+
*/
|
|
3001
|
+
attachToSocket(comms) {
|
|
3002
|
+
comms.onSpawnRequest((req) => {
|
|
3003
|
+
void this.handleSpawn(req);
|
|
3004
|
+
});
|
|
3005
|
+
comms.onAssignmentAvailable((event) => {
|
|
3006
|
+
void this.handleAssignmentAvailable(event);
|
|
3007
|
+
});
|
|
3008
|
+
}
|
|
3009
|
+
getActiveSessionCount() {
|
|
3010
|
+
return this.activeSessions;
|
|
3011
|
+
}
|
|
3012
|
+
getMaxConcurrentSessions() {
|
|
3013
|
+
return this.maxConcurrentSessions;
|
|
3014
|
+
}
|
|
3015
|
+
canAcceptNew() {
|
|
3016
|
+
if (this.activeSessions >= this.maxConcurrentSessions) return false;
|
|
3017
|
+
const breach = this.checkResourceLimits();
|
|
3018
|
+
return breach == null;
|
|
3019
|
+
}
|
|
3020
|
+
/**
|
|
3021
|
+
* Returns the resource limit that would be breached by accepting a
|
|
3022
|
+
* new claim, or null if all limits are within bounds. Exposed so
|
|
3023
|
+
* `daemon status` can surface which limit is the active gate.
|
|
3024
|
+
*/
|
|
3025
|
+
checkResourceLimits() {
|
|
3026
|
+
if (!this.resourceSampler) return null;
|
|
3027
|
+
const snap = this.resourceSampler.snapshot();
|
|
3028
|
+
if (snap.sampleCount === 0) return null;
|
|
3029
|
+
if (snap.cpuPercent > this.maxCpuPercent) {
|
|
3030
|
+
return {
|
|
3031
|
+
limit: "cpu",
|
|
3032
|
+
current: snap.cpuPercent,
|
|
3033
|
+
max: this.maxCpuPercent
|
|
3034
|
+
};
|
|
3035
|
+
}
|
|
3036
|
+
if (this.maxMemoryBytes != null && snap.memoryRssBytes > this.maxMemoryBytes) {
|
|
3037
|
+
return {
|
|
3038
|
+
limit: "memory",
|
|
3039
|
+
current: snap.memoryRssBytes,
|
|
3040
|
+
max: this.maxMemoryBytes
|
|
3041
|
+
};
|
|
3042
|
+
}
|
|
3043
|
+
return null;
|
|
3044
|
+
}
|
|
3045
|
+
/**
|
|
3046
|
+
* Test-and-runtime hook: process an `agent:spawn` payload as if it had
|
|
3047
|
+
* arrived over the socket. Exposed for unit tests and for the
|
|
3048
|
+
* future re-emit-after-claim path; the live wiring is in the
|
|
3049
|
+
* constructor's `onSpawnRequest` registration.
|
|
3050
|
+
*/
|
|
3051
|
+
async handleSpawn(req) {
|
|
3052
|
+
if (this.activeSessions >= this.maxConcurrentSessions) {
|
|
3053
|
+
this.emitResourceLimitBreached(req.sessionId, "agent:spawn", {
|
|
3054
|
+
limit: "sessions",
|
|
3055
|
+
current: this.activeSessions,
|
|
3056
|
+
max: this.maxConcurrentSessions
|
|
3057
|
+
});
|
|
3058
|
+
return;
|
|
3059
|
+
}
|
|
3060
|
+
const resourceBreach = this.checkResourceLimits();
|
|
3061
|
+
if (resourceBreach) {
|
|
3062
|
+
this.emitResourceLimitBreached(
|
|
3063
|
+
req.sessionId,
|
|
3064
|
+
"agent:spawn",
|
|
3065
|
+
resourceBreach
|
|
3066
|
+
);
|
|
3067
|
+
return;
|
|
3068
|
+
}
|
|
3069
|
+
this.activeSessions += 1;
|
|
3070
|
+
this.audit.record({
|
|
3071
|
+
sessionId: req.sessionId,
|
|
3072
|
+
type: "daemon_lifecycle" /* DAEMON_LIFECYCLE */,
|
|
3073
|
+
action: DAEMON_AUDIT_ACTIONS.SESSION_CLAIMED,
|
|
3074
|
+
details: {
|
|
3075
|
+
machineId: this.machineId,
|
|
3076
|
+
orgId: req.orgId,
|
|
3077
|
+
task: truncate2(req.task, 120)
|
|
3078
|
+
},
|
|
3079
|
+
outcome: "success"
|
|
3080
|
+
});
|
|
3081
|
+
try {
|
|
3082
|
+
await this.spawnRunner(req);
|
|
3083
|
+
this.audit.record({
|
|
3084
|
+
sessionId: req.sessionId,
|
|
3085
|
+
type: "daemon_lifecycle" /* DAEMON_LIFECYCLE */,
|
|
3086
|
+
action: DAEMON_AUDIT_ACTIONS.SESSION_COMPLETED,
|
|
3087
|
+
details: { machineId: this.machineId },
|
|
3088
|
+
outcome: "success"
|
|
3089
|
+
});
|
|
3090
|
+
} catch (err) {
|
|
3091
|
+
this.audit.record({
|
|
3092
|
+
sessionId: req.sessionId,
|
|
3093
|
+
type: "daemon_lifecycle" /* DAEMON_LIFECYCLE */,
|
|
3094
|
+
action: DAEMON_AUDIT_ACTIONS.SESSION_FAILED,
|
|
3095
|
+
details: {
|
|
3096
|
+
machineId: this.machineId,
|
|
3097
|
+
error: err.message
|
|
3098
|
+
},
|
|
3099
|
+
outcome: "failure"
|
|
3100
|
+
});
|
|
3101
|
+
} finally {
|
|
3102
|
+
this.activeSessions -= 1;
|
|
3103
|
+
}
|
|
3104
|
+
}
|
|
3105
|
+
async handleAssignmentAvailable(event) {
|
|
3106
|
+
if (this.activeSessions >= this.maxConcurrentSessions) {
|
|
3107
|
+
this.emitResourceLimitBreached(
|
|
3108
|
+
event.assignmentId,
|
|
3109
|
+
"assignment:claim-available",
|
|
3110
|
+
{
|
|
3111
|
+
limit: "sessions",
|
|
3112
|
+
current: this.activeSessions,
|
|
3113
|
+
max: this.maxConcurrentSessions
|
|
3114
|
+
}
|
|
3115
|
+
);
|
|
3116
|
+
return;
|
|
3117
|
+
}
|
|
3118
|
+
const resourceBreach = this.checkResourceLimits();
|
|
3119
|
+
if (resourceBreach) {
|
|
3120
|
+
this.emitResourceLimitBreached(
|
|
3121
|
+
event.assignmentId,
|
|
3122
|
+
"assignment:claim-available",
|
|
3123
|
+
resourceBreach
|
|
3124
|
+
);
|
|
3125
|
+
return;
|
|
3126
|
+
}
|
|
3127
|
+
if (!this.platformClient || !this.deviceToken) {
|
|
3128
|
+
console.log(
|
|
3129
|
+
`[daemon] assignment:claim-available assignmentId=${event.assignmentId} (platformClient not wired \u2014 Phase 5b config missing)`
|
|
3130
|
+
);
|
|
3131
|
+
return;
|
|
3132
|
+
}
|
|
3133
|
+
try {
|
|
3134
|
+
const result = await this.platformClient.claimAssignment(
|
|
3135
|
+
event.assignmentId,
|
|
3136
|
+
this.machineId,
|
|
3137
|
+
this.deviceToken
|
|
3138
|
+
);
|
|
3139
|
+
this.audit.record({
|
|
3140
|
+
sessionId: event.assignmentId,
|
|
3141
|
+
type: "daemon_lifecycle" /* DAEMON_LIFECYCLE */,
|
|
3142
|
+
action: DAEMON_AUDIT_ACTIONS.SESSION_CLAIMED,
|
|
3143
|
+
details: {
|
|
3144
|
+
machineId: this.machineId,
|
|
3145
|
+
orgId: event.orgId,
|
|
3146
|
+
workItemId: event.workItemId,
|
|
3147
|
+
claimedAt: result.claimedAt,
|
|
3148
|
+
via: "assignment:claim-available"
|
|
3149
|
+
},
|
|
3150
|
+
outcome: "success"
|
|
3151
|
+
});
|
|
3152
|
+
} catch (err) {
|
|
3153
|
+
this.audit.record({
|
|
3154
|
+
sessionId: event.assignmentId,
|
|
3155
|
+
type: "daemon_lifecycle" /* DAEMON_LIFECYCLE */,
|
|
3156
|
+
action: DAEMON_AUDIT_ACTIONS.SESSION_FAILED,
|
|
3157
|
+
details: {
|
|
3158
|
+
machineId: this.machineId,
|
|
3159
|
+
phase: "claim",
|
|
3160
|
+
error: err.message
|
|
3161
|
+
},
|
|
3162
|
+
outcome: "failure"
|
|
3163
|
+
});
|
|
3164
|
+
}
|
|
3165
|
+
}
|
|
3166
|
+
/**
|
|
3167
|
+
* Phase 5c — release a previously claimed assignment when the
|
|
3168
|
+
* daemon cannot proceed (e.g. drain interrupted, capacity check
|
|
3169
|
+
* failed mid-flight). Best-effort: failures here are logged but
|
|
3170
|
+
* the daemon's local state already moved on.
|
|
3171
|
+
*/
|
|
3172
|
+
async releaseClaim(assignmentId, reason) {
|
|
3173
|
+
if (!this.platformClient || !this.deviceToken) return;
|
|
3174
|
+
try {
|
|
3175
|
+
await this.platformClient.releaseAssignment(
|
|
3176
|
+
assignmentId,
|
|
3177
|
+
this.machineId,
|
|
3178
|
+
reason,
|
|
3179
|
+
this.deviceToken
|
|
3180
|
+
);
|
|
3181
|
+
} catch (err) {
|
|
3182
|
+
console.warn(
|
|
3183
|
+
`[daemon] release ${assignmentId} failed: ${err.message}`
|
|
3184
|
+
);
|
|
3185
|
+
}
|
|
3186
|
+
}
|
|
3187
|
+
emitResourceLimitBreached(refId, trigger, breach) {
|
|
3188
|
+
this.audit.record({
|
|
3189
|
+
sessionId: refId,
|
|
3190
|
+
type: "daemon_lifecycle" /* DAEMON_LIFECYCLE */,
|
|
3191
|
+
action: DAEMON_AUDIT_ACTIONS.RESOURCE_LIMIT_BREACHED,
|
|
3192
|
+
details: {
|
|
3193
|
+
machineId: this.machineId,
|
|
3194
|
+
trigger,
|
|
3195
|
+
limit: breach.limit,
|
|
3196
|
+
current: breach.current,
|
|
3197
|
+
max: breach.max,
|
|
3198
|
+
activeSessions: this.activeSessions,
|
|
3199
|
+
maxConcurrentSessions: this.maxConcurrentSessions
|
|
3200
|
+
},
|
|
3201
|
+
outcome: "failure"
|
|
3202
|
+
});
|
|
3203
|
+
}
|
|
3204
|
+
};
|
|
3205
|
+
async function defaultSpawnStub(request) {
|
|
3206
|
+
console.log(
|
|
3207
|
+
`[daemon] STUB: would spawn session ${request.sessionId} for task: ${truncate2(request.task, 80)}`
|
|
3208
|
+
);
|
|
3209
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
3210
|
+
}
|
|
3211
|
+
function truncate2(s, n) {
|
|
3212
|
+
if (!s) return s;
|
|
3213
|
+
return s.length <= n ? s : `${s.slice(0, n - 1)}\u2026`;
|
|
3214
|
+
}
|
|
3215
|
+
|
|
3216
|
+
// _apps/@onklave/agent-cli/src/services/daemon-resource-sampler.service.ts
|
|
3217
|
+
import * as os4 from "os";
|
|
3218
|
+
var DEFAULT_SAMPLE_INTERVAL_MS = 5e3;
|
|
3219
|
+
var DEFAULT_WINDOW_MS = 6e4;
|
|
3220
|
+
var DaemonResourceSampler = class {
|
|
3221
|
+
constructor(opts = {}) {
|
|
3222
|
+
this.samples = [];
|
|
3223
|
+
this.timer = null;
|
|
3224
|
+
this.sampleIntervalMs = opts.sampleIntervalMs ?? DEFAULT_SAMPLE_INTERVAL_MS;
|
|
3225
|
+
this.windowMs = opts.windowMs ?? DEFAULT_WINDOW_MS;
|
|
3226
|
+
this.cpuSource = opts.cpuSource ?? defaultCpuPercent;
|
|
3227
|
+
this.memorySource = opts.memorySource ?? defaultMemoryRss;
|
|
3228
|
+
}
|
|
3229
|
+
/**
|
|
3230
|
+
* Start the periodic sampler. Idempotent — second start is a no-op.
|
|
3231
|
+
* The timer is `unref()`'d so a daemon that has nothing else to do
|
|
3232
|
+
* (no in-flight sessions, no inbound events) does not stay alive only
|
|
3233
|
+
* because of the sampler.
|
|
3234
|
+
*/
|
|
3235
|
+
start() {
|
|
3236
|
+
if (this.timer) return;
|
|
3237
|
+
this.sample();
|
|
3238
|
+
this.timer = setInterval(() => this.sample(), this.sampleIntervalMs);
|
|
3239
|
+
this.timer.unref?.();
|
|
3240
|
+
}
|
|
3241
|
+
stop() {
|
|
3242
|
+
if (this.timer) {
|
|
3243
|
+
clearInterval(this.timer);
|
|
3244
|
+
this.timer = null;
|
|
3245
|
+
}
|
|
3246
|
+
}
|
|
3247
|
+
/**
|
|
3248
|
+
* Returns the windowed average of CPU% and the latest memory RSS.
|
|
3249
|
+
* Memory is *not* averaged — the RSS at sample time is what matters
|
|
3250
|
+
* for "can we fit another session right now".
|
|
3251
|
+
*/
|
|
3252
|
+
snapshot() {
|
|
3253
|
+
if (this.samples.length === 0) {
|
|
3254
|
+
return {
|
|
3255
|
+
cpuPercent: 0,
|
|
3256
|
+
memoryRssBytes: 0,
|
|
3257
|
+
sampleCount: 0,
|
|
3258
|
+
windowMs: this.windowMs
|
|
3259
|
+
};
|
|
3260
|
+
}
|
|
3261
|
+
const cpuAvg = this.samples.reduce((sum, s) => sum + s.cpuPercent, 0) / this.samples.length;
|
|
3262
|
+
const memLatest = this.samples[this.samples.length - 1].memoryRssBytes;
|
|
3263
|
+
return {
|
|
3264
|
+
cpuPercent: cpuAvg,
|
|
3265
|
+
memoryRssBytes: memLatest,
|
|
3266
|
+
sampleCount: this.samples.length,
|
|
3267
|
+
windowMs: this.windowMs
|
|
3268
|
+
};
|
|
3269
|
+
}
|
|
3270
|
+
/** Take one sample now. Exposed for tests; called internally by start. */
|
|
3271
|
+
sample() {
|
|
3272
|
+
const now = Date.now();
|
|
3273
|
+
this.samples.push({
|
|
3274
|
+
at: now,
|
|
3275
|
+
cpuPercent: this.cpuSource(),
|
|
3276
|
+
memoryRssBytes: this.memorySource()
|
|
3277
|
+
});
|
|
3278
|
+
const cutoff = now - this.windowMs;
|
|
3279
|
+
while (this.samples.length > 0 && this.samples[0].at < cutoff) {
|
|
3280
|
+
this.samples.shift();
|
|
3281
|
+
}
|
|
3282
|
+
}
|
|
3283
|
+
};
|
|
3284
|
+
function defaultCpuPercent() {
|
|
3285
|
+
const cpus2 = os4.cpus().length || 1;
|
|
3286
|
+
const load1m = os4.loadavg()[0];
|
|
3287
|
+
return load1m / cpus2 * 100;
|
|
3288
|
+
}
|
|
3289
|
+
function defaultMemoryRss() {
|
|
3290
|
+
return process.memoryUsage().rss;
|
|
3291
|
+
}
|
|
3292
|
+
|
|
3293
|
+
// _apps/@onklave/agent-cli/src/services/daemon-spawner.service.ts
|
|
3294
|
+
var DaemonSpawner = class {
|
|
3295
|
+
constructor(opts) {
|
|
3296
|
+
this.sessionManager = opts.sessionManager ?? new SessionManager();
|
|
3297
|
+
this.platformUrl = opts.platformUrl;
|
|
3298
|
+
}
|
|
3299
|
+
async runSpawnRequest(req) {
|
|
3300
|
+
const config = {
|
|
3301
|
+
task: req.task,
|
|
3302
|
+
context: req.context,
|
|
3303
|
+
persona: req.persona,
|
|
3304
|
+
workflow: req.workflow,
|
|
3305
|
+
model: req.model,
|
|
3306
|
+
timeout: req.timeout,
|
|
3307
|
+
guardrails: [],
|
|
3308
|
+
allowedTools: [],
|
|
3309
|
+
deniedTools: [],
|
|
3310
|
+
apiKey: req.apiKey ?? null,
|
|
3311
|
+
headless: true,
|
|
3312
|
+
platformUrl: this.platformUrl,
|
|
3313
|
+
orgId: req.orgId,
|
|
3314
|
+
systemPromptAppend: null
|
|
3315
|
+
};
|
|
3316
|
+
return new Promise((resolve3, reject) => {
|
|
3317
|
+
void this.sessionManager.spawnSession(req.sessionId, config, {
|
|
3318
|
+
onStdout: (data) => {
|
|
3319
|
+
process.stdout.write(data);
|
|
3320
|
+
},
|
|
3321
|
+
onStderr: (data) => {
|
|
3322
|
+
process.stderr.write(data);
|
|
3323
|
+
},
|
|
3324
|
+
onExit: (code, signal) => {
|
|
3325
|
+
if (code === 0) {
|
|
3326
|
+
resolve3();
|
|
3327
|
+
} else {
|
|
3328
|
+
reject(
|
|
3329
|
+
new Error(
|
|
3330
|
+
`session ${req.sessionId} exited with code=${code} signal=${signal}`
|
|
3331
|
+
)
|
|
3332
|
+
);
|
|
3333
|
+
}
|
|
3334
|
+
}
|
|
3335
|
+
});
|
|
3336
|
+
});
|
|
3337
|
+
}
|
|
3338
|
+
};
|
|
3339
|
+
|
|
3340
|
+
// _apps/@onklave/agent-cli/src/services/daemon-token-observer.service.ts
|
|
3341
|
+
var WARNING_THRESHOLD_DAYS = 30;
|
|
3342
|
+
var DaemonTokenObserver = class {
|
|
3343
|
+
constructor(opts) {
|
|
3344
|
+
this.warningEmitted = false;
|
|
3345
|
+
this.expiredEmitted = false;
|
|
3346
|
+
this.machineId = opts.machineId;
|
|
3347
|
+
this.audit = opts.audit;
|
|
3348
|
+
this.expiresAt = opts.deviceTokenExpiresAt ? new Date(opts.deviceTokenExpiresAt) : null;
|
|
3349
|
+
this.now = opts.now ?? (() => /* @__PURE__ */ new Date());
|
|
3350
|
+
}
|
|
3351
|
+
/**
|
|
3352
|
+
* Update the tracked expiry after a successful refresh. Resets the
|
|
3353
|
+
* one-shot emitted flags so a future warning / expiry can re-emit.
|
|
3354
|
+
*/
|
|
3355
|
+
updateExpiry(newExpiresAt) {
|
|
3356
|
+
this.expiresAt = new Date(newExpiresAt);
|
|
3357
|
+
this.warningEmitted = false;
|
|
3358
|
+
this.expiredEmitted = false;
|
|
3359
|
+
}
|
|
3360
|
+
/**
|
|
3361
|
+
* Snapshot the current expiry status. Idempotent — does not emit audit
|
|
3362
|
+
* events. Use {@link emitIfNeeded} for the side-effectful check.
|
|
3363
|
+
*/
|
|
3364
|
+
status() {
|
|
3365
|
+
if (!this.expiresAt || Number.isNaN(this.expiresAt.getTime())) {
|
|
3366
|
+
return {
|
|
3367
|
+
state: "unknown",
|
|
3368
|
+
message: "device token expiry not recorded \u2014 re-register to refresh"
|
|
3369
|
+
};
|
|
3370
|
+
}
|
|
3371
|
+
const msRemaining = this.expiresAt.getTime() - this.now().getTime();
|
|
3372
|
+
if (msRemaining <= 0) {
|
|
3373
|
+
return { state: "expired", expiresAt: this.expiresAt.toISOString() };
|
|
3374
|
+
}
|
|
3375
|
+
const daysRemaining = Math.floor(msRemaining / (24 * 60 * 60 * 1e3));
|
|
3376
|
+
if (daysRemaining <= WARNING_THRESHOLD_DAYS) {
|
|
3377
|
+
return {
|
|
3378
|
+
state: "warning",
|
|
3379
|
+
daysRemaining,
|
|
3380
|
+
expiresAt: this.expiresAt.toISOString()
|
|
3381
|
+
};
|
|
3382
|
+
}
|
|
3383
|
+
return {
|
|
3384
|
+
state: "ok",
|
|
3385
|
+
daysRemaining,
|
|
3386
|
+
expiresAt: this.expiresAt.toISOString()
|
|
3387
|
+
};
|
|
3388
|
+
}
|
|
3389
|
+
/**
|
|
3390
|
+
* Emit audit events for state changes (warning crossed, expiry hit).
|
|
3391
|
+
* Emits at most one warning and one expired event per observer
|
|
3392
|
+
* instance — repeated calls are silent until process restart.
|
|
3393
|
+
*/
|
|
3394
|
+
emitIfNeeded() {
|
|
3395
|
+
const status = this.status();
|
|
3396
|
+
if (status.state === "expired" && !this.expiredEmitted) {
|
|
3397
|
+
this.audit.record({
|
|
3398
|
+
sessionId: this.machineId,
|
|
3399
|
+
type: "daemon_lifecycle" /* DAEMON_LIFECYCLE */,
|
|
3400
|
+
action: DAEMON_AUDIT_ACTIONS.AUTH_EXPIRED,
|
|
3401
|
+
details: { machineId: this.machineId, expiresAt: status.expiresAt },
|
|
3402
|
+
outcome: "failure"
|
|
3403
|
+
});
|
|
3404
|
+
this.expiredEmitted = true;
|
|
3405
|
+
} else if (status.state === "warning" && !this.warningEmitted) {
|
|
3406
|
+
this.audit.record({
|
|
3407
|
+
sessionId: this.machineId,
|
|
3408
|
+
type: "daemon_lifecycle" /* DAEMON_LIFECYCLE */,
|
|
3409
|
+
action: DAEMON_AUDIT_ACTIONS.AUTH_WARNING,
|
|
3410
|
+
details: {
|
|
3411
|
+
machineId: this.machineId,
|
|
3412
|
+
daysRemaining: status.daysRemaining,
|
|
3413
|
+
expiresAt: status.expiresAt
|
|
3414
|
+
},
|
|
3415
|
+
outcome: "success"
|
|
3416
|
+
});
|
|
3417
|
+
this.warningEmitted = true;
|
|
3418
|
+
}
|
|
3419
|
+
return status;
|
|
3420
|
+
}
|
|
3421
|
+
/** True iff the token has expired right now. */
|
|
3422
|
+
isExpired() {
|
|
3423
|
+
return this.status().state === "expired";
|
|
3424
|
+
}
|
|
3425
|
+
};
|
|
3426
|
+
|
|
3427
|
+
// _apps/@onklave/agent-cli/src/services/daemon-token-refresher.service.ts
|
|
3428
|
+
var REFRESH_THRESHOLD_DAYS = 7;
|
|
3429
|
+
var DaemonTokenRefresher = class {
|
|
3430
|
+
constructor(opts) {
|
|
3431
|
+
this.opts = opts;
|
|
3432
|
+
this.inFlight = false;
|
|
3433
|
+
this.lastAttemptAt = null;
|
|
3434
|
+
}
|
|
3435
|
+
/**
|
|
3436
|
+
* Idempotent tick — call from the heartbeat / runtime publisher loop.
|
|
3437
|
+
* Does nothing if the token is healthy (>7 days remaining), if the
|
|
3438
|
+
* token's expiry is unknown, or if a refresh is already in flight.
|
|
3439
|
+
*/
|
|
3440
|
+
async maybeRefresh() {
|
|
3441
|
+
if (this.inFlight) return;
|
|
3442
|
+
const status = this.opts.observer.status();
|
|
3443
|
+
if (status.state === "unknown") return;
|
|
3444
|
+
if (status.state === "expired") return;
|
|
3445
|
+
if (status.daysRemaining > REFRESH_THRESHOLD_DAYS) return;
|
|
3446
|
+
this.inFlight = true;
|
|
3447
|
+
this.lastAttemptAt = /* @__PURE__ */ new Date();
|
|
3448
|
+
try {
|
|
3449
|
+
const currentToken = this.opts.getDeviceToken();
|
|
3450
|
+
const result = await this.opts.platformClient.refreshDeviceToken(
|
|
3451
|
+
currentToken
|
|
3452
|
+
);
|
|
3453
|
+
await this.opts.authService.saveDeviceToken(
|
|
3454
|
+
result.deviceToken,
|
|
3455
|
+
this.opts.machineId,
|
|
3456
|
+
result.deviceTokenExpiresAt
|
|
3457
|
+
);
|
|
3458
|
+
this.opts.observer.updateExpiry(result.deviceTokenExpiresAt);
|
|
3459
|
+
this.opts.onTokenRefreshed?.(
|
|
3460
|
+
result.deviceToken,
|
|
3461
|
+
result.deviceTokenExpiresAt
|
|
3462
|
+
);
|
|
3463
|
+
} catch (err) {
|
|
3464
|
+
console.warn(
|
|
3465
|
+
`[daemon] device-token refresh failed: ${err.message} (will retry)`
|
|
3466
|
+
);
|
|
3467
|
+
} finally {
|
|
3468
|
+
this.inFlight = false;
|
|
3469
|
+
}
|
|
3470
|
+
}
|
|
3471
|
+
/** Most recent refresh-attempt time, or null if never tried. */
|
|
3472
|
+
getLastAttemptAt() {
|
|
3473
|
+
return this.lastAttemptAt;
|
|
3474
|
+
}
|
|
3475
|
+
};
|
|
3476
|
+
|
|
3477
|
+
// _apps/@onklave/agent-cli/src/services/daemon-update-checker.service.ts
|
|
3478
|
+
var REGISTRY_URL = "https://registry.npmjs.org";
|
|
3479
|
+
var PACKAGE_NAME = "@onklave/agent-cli";
|
|
3480
|
+
var DaemonUpdateChecker = class {
|
|
3481
|
+
constructor(opts) {
|
|
3482
|
+
this.latestVersion = null;
|
|
3483
|
+
this.lastCheckedAt = null;
|
|
3484
|
+
this.emittedForVersion = null;
|
|
3485
|
+
this.currentVersion = opts.currentVersion;
|
|
3486
|
+
this.audit = opts.audit;
|
|
3487
|
+
this.registryFetcher = opts.registryFetcher ?? defaultRegistryFetcher;
|
|
3488
|
+
}
|
|
3489
|
+
/**
|
|
3490
|
+
* Hit the registry. On success, sets latestVersion and (if newer than
|
|
3491
|
+
* current) emits `daemon.update_available` — exactly once per version,
|
|
3492
|
+
* even across repeated checks. On failure, logs and returns; the next
|
|
3493
|
+
* tick can retry.
|
|
3494
|
+
*/
|
|
3495
|
+
async check() {
|
|
3496
|
+
try {
|
|
3497
|
+
const latest = await this.registryFetcher(PACKAGE_NAME);
|
|
3498
|
+
this.latestVersion = latest;
|
|
3499
|
+
this.lastCheckedAt = /* @__PURE__ */ new Date();
|
|
3500
|
+
if (isNewerVersion(latest, this.currentVersion) && this.emittedForVersion !== latest) {
|
|
3501
|
+
this.audit.record({
|
|
3502
|
+
sessionId: this.currentVersion,
|
|
3503
|
+
type: "daemon_lifecycle" /* DAEMON_LIFECYCLE */,
|
|
3504
|
+
action: DAEMON_AUDIT_ACTIONS.UPDATE_AVAILABLE,
|
|
3505
|
+
details: {
|
|
3506
|
+
currentVersion: this.currentVersion,
|
|
3507
|
+
latestVersion: latest
|
|
3508
|
+
},
|
|
3509
|
+
outcome: "success"
|
|
3510
|
+
});
|
|
3511
|
+
this.emittedForVersion = latest;
|
|
3512
|
+
}
|
|
3513
|
+
} catch (err) {
|
|
3514
|
+
console.warn(
|
|
3515
|
+
`[daemon] update check failed: ${err.message} (will retry)`
|
|
3516
|
+
);
|
|
3517
|
+
}
|
|
3518
|
+
}
|
|
3519
|
+
info() {
|
|
3520
|
+
return {
|
|
3521
|
+
currentVersion: this.currentVersion,
|
|
3522
|
+
latestVersion: this.latestVersion,
|
|
3523
|
+
updateAvailable: !!this.latestVersion && isNewerVersion(this.latestVersion, this.currentVersion),
|
|
3524
|
+
checkedAt: this.lastCheckedAt?.toISOString() ?? null
|
|
3525
|
+
};
|
|
3526
|
+
}
|
|
3527
|
+
};
|
|
3528
|
+
async function defaultRegistryFetcher(packageName) {
|
|
3529
|
+
const url = `${REGISTRY_URL}/${encodeURIComponent(packageName)}/latest`;
|
|
3530
|
+
const response = await fetch(url, {
|
|
3531
|
+
method: "GET",
|
|
3532
|
+
headers: { Accept: "application/json" },
|
|
3533
|
+
signal: AbortSignal.timeout(1e4)
|
|
3534
|
+
});
|
|
3535
|
+
if (!response.ok) {
|
|
3536
|
+
throw new Error(`registry ${response.status} ${response.statusText}`);
|
|
3537
|
+
}
|
|
3538
|
+
const body = await response.json();
|
|
3539
|
+
if (!body.version) throw new Error("registry response missing version");
|
|
3540
|
+
return body.version;
|
|
3541
|
+
}
|
|
3542
|
+
function isNewerVersion(a, b) {
|
|
3543
|
+
const pa = parseSemverNumeric(a);
|
|
3544
|
+
const pb = parseSemverNumeric(b);
|
|
3545
|
+
if (!pa || !pb) return false;
|
|
3546
|
+
for (let i = 0; i < 3; i += 1) {
|
|
3547
|
+
if (pa[i] > pb[i]) return true;
|
|
3548
|
+
if (pa[i] < pb[i]) return false;
|
|
3549
|
+
}
|
|
3550
|
+
return false;
|
|
3551
|
+
}
|
|
3552
|
+
function parseSemverNumeric(v) {
|
|
3553
|
+
const match = /^(\d+)\.(\d+)\.(\d+)/.exec(v);
|
|
3554
|
+
if (!match) return null;
|
|
3555
|
+
return [
|
|
3556
|
+
Number.parseInt(match[1], 10),
|
|
3557
|
+
Number.parseInt(match[2], 10),
|
|
3558
|
+
Number.parseInt(match[3], 10)
|
|
3559
|
+
];
|
|
3560
|
+
}
|
|
3561
|
+
|
|
2941
3562
|
// _apps/@onklave/agent-cli/src/services/daemon-state.service.ts
|
|
2942
3563
|
import * as fs5 from "fs";
|
|
2943
3564
|
import * as path6 from "path";
|
|
2944
|
-
import * as
|
|
3565
|
+
import * as os5 from "os";
|
|
2945
3566
|
var VALID_TRANSITIONS = {
|
|
2946
3567
|
installing: ["registered"],
|
|
2947
3568
|
registered: ["starting"],
|
|
@@ -2952,10 +3573,10 @@ var VALID_TRANSITIONS = {
|
|
|
2952
3573
|
stopped: ["starting"]
|
|
2953
3574
|
};
|
|
2954
3575
|
function defaultStateFilePath() {
|
|
2955
|
-
return path6.join(
|
|
3576
|
+
return path6.join(os5.homedir(), ".config", "onklave", "daemon.state.json");
|
|
2956
3577
|
}
|
|
2957
3578
|
function defaultPidFilePath() {
|
|
2958
|
-
return path6.join(
|
|
3579
|
+
return path6.join(os5.homedir(), ".config", "onklave", "daemon.pid");
|
|
2959
3580
|
}
|
|
2960
3581
|
var DaemonStateError = class extends Error {
|
|
2961
3582
|
constructor(message) {
|
|
@@ -2968,6 +3589,7 @@ var DaemonStateService = class {
|
|
|
2968
3589
|
this.stateFile = stateFile;
|
|
2969
3590
|
this.machineId = machineId;
|
|
2970
3591
|
this.listeners = [];
|
|
3592
|
+
this.latestRuntime = null;
|
|
2971
3593
|
this.current = initial;
|
|
2972
3594
|
this.enteredAt = /* @__PURE__ */ new Date();
|
|
2973
3595
|
}
|
|
@@ -3036,12 +3658,22 @@ var DaemonStateService = class {
|
|
|
3036
3658
|
enteredAt: this.enteredAt.toISOString(),
|
|
3037
3659
|
pid: process.pid,
|
|
3038
3660
|
...this.machineId ? { machineId: this.machineId } : {},
|
|
3039
|
-
...reason ? { reason } : {}
|
|
3661
|
+
...reason ? { reason } : {},
|
|
3662
|
+
...this.latestRuntime ? { runtime: this.latestRuntime } : {}
|
|
3040
3663
|
};
|
|
3041
3664
|
const tmp = `${this.stateFile}.tmp`;
|
|
3042
3665
|
fs5.writeFileSync(tmp, JSON.stringify(payload, null, 2));
|
|
3043
3666
|
fs5.renameSync(tmp, this.stateFile);
|
|
3044
3667
|
}
|
|
3668
|
+
/**
|
|
3669
|
+
* Publish the latest runtime snapshot. Persists to the state file
|
|
3670
|
+
* so `daemon status` from another process can read it. Idempotent —
|
|
3671
|
+
* no transition is fired.
|
|
3672
|
+
*/
|
|
3673
|
+
publishRuntime(snapshot) {
|
|
3674
|
+
this.latestRuntime = snapshot;
|
|
3675
|
+
this.persist();
|
|
3676
|
+
}
|
|
3045
3677
|
/**
|
|
3046
3678
|
* Remove the persisted state file. Called on terminal `stopped` once
|
|
3047
3679
|
* the daemon process is exiting cleanly so `daemon status` reports
|
|
@@ -3179,10 +3811,11 @@ async function daemonCommand(args) {
|
|
|
3179
3811
|
const [sub] = args;
|
|
3180
3812
|
if (sub === "status") return daemonStatus();
|
|
3181
3813
|
if (sub === "stop" || sub === "drain") return daemonStop();
|
|
3814
|
+
if (sub === "update") return daemonUpdate();
|
|
3182
3815
|
if (sub && !sub.startsWith("--")) {
|
|
3183
3816
|
console.error(`Unknown subcommand: ${sub}`);
|
|
3184
3817
|
console.error(
|
|
3185
|
-
"Usage: onklave daemon [status|stop|drain] | --emit-systemd-unit | --emit-launchd-plist | --emit-nssm-script"
|
|
3818
|
+
"Usage: onklave daemon [status|stop|drain|update] | --emit-systemd-unit | --emit-launchd-plist | --emit-nssm-script"
|
|
3186
3819
|
);
|
|
3187
3820
|
process.exitCode = 1;
|
|
3188
3821
|
return;
|
|
@@ -3231,25 +3864,76 @@ async function daemonStart() {
|
|
|
3231
3864
|
};
|
|
3232
3865
|
auditStreamer.record(event);
|
|
3233
3866
|
});
|
|
3234
|
-
|
|
3867
|
+
const spawner = new DaemonSpawner({ platformUrl });
|
|
3868
|
+
const platformClient = new PlatformClient(platformUrl, creds.token);
|
|
3869
|
+
const resourceSampler = new DaemonResourceSampler();
|
|
3870
|
+
const claimHandler = new DaemonClaimHandler({
|
|
3871
|
+
machineId: creds.machineId,
|
|
3872
|
+
audit: auditStreamer,
|
|
3873
|
+
spawnRunner: (req) => spawner.runSpawnRequest(req),
|
|
3874
|
+
platformClient,
|
|
3875
|
+
deviceToken: creds.deviceToken,
|
|
3876
|
+
resourceSampler,
|
|
3877
|
+
maxCpuPercent: 80
|
|
3878
|
+
// spec §8.1 default
|
|
3879
|
+
});
|
|
3880
|
+
const tokenObserver = new DaemonTokenObserver({
|
|
3881
|
+
machineId: creds.machineId,
|
|
3882
|
+
audit: auditStreamer,
|
|
3883
|
+
deviceTokenExpiresAt: creds.deviceTokenExpiresAt
|
|
3884
|
+
});
|
|
3885
|
+
const updateChecker = new DaemonUpdateChecker({
|
|
3886
|
+
currentVersion: readPackageVersion() ?? "0.0.0",
|
|
3887
|
+
audit: auditStreamer
|
|
3888
|
+
});
|
|
3889
|
+
let currentDeviceToken = creds.deviceToken;
|
|
3890
|
+
const tokenRefresher = new DaemonTokenRefresher({
|
|
3891
|
+
machineId: creds.machineId,
|
|
3892
|
+
observer: tokenObserver,
|
|
3893
|
+
platformClient,
|
|
3894
|
+
authService,
|
|
3895
|
+
getDeviceToken: () => currentDeviceToken,
|
|
3896
|
+
onTokenRefreshed: (newToken) => {
|
|
3897
|
+
currentDeviceToken = newToken;
|
|
3898
|
+
}
|
|
3899
|
+
});
|
|
3900
|
+
const initialTokenStatus = tokenObserver.emitIfNeeded();
|
|
3901
|
+
if (initialTokenStatus.state === "expired") {
|
|
3902
|
+
console.error(
|
|
3903
|
+
`Device token expired (${initialTokenStatus.expiresAt}). Re-register: onklave register --token <bootstrap>`
|
|
3904
|
+
);
|
|
3905
|
+
removePid(pidFile);
|
|
3906
|
+
stateService.clearPersisted();
|
|
3907
|
+
process.exit(1);
|
|
3908
|
+
}
|
|
3909
|
+
if (initialTokenStatus.state === "warning") {
|
|
3910
|
+
console.warn(
|
|
3911
|
+
`\u26A0 Device token expires in ${initialTokenStatus.daysRemaining} days (${initialTokenStatus.expiresAt}).`
|
|
3912
|
+
);
|
|
3913
|
+
}
|
|
3235
3914
|
let shuttingDown = false;
|
|
3236
3915
|
let sigintCount = 0;
|
|
3237
3916
|
let sigintTimer = null;
|
|
3917
|
+
let runtimePublisher = null;
|
|
3918
|
+
let updateCheckTimer = null;
|
|
3238
3919
|
const heartbeat = new HeartbeatService({
|
|
3239
3920
|
platformUrl,
|
|
3240
3921
|
deviceToken: creds.deviceToken,
|
|
3241
3922
|
machineId: creds.machineId,
|
|
3242
|
-
getActiveSessionCount: () =>
|
|
3923
|
+
getActiveSessionCount: () => claimHandler.getActiveSessionCount()
|
|
3243
3924
|
});
|
|
3244
3925
|
const drainAndExit = async (reason) => {
|
|
3245
3926
|
if (shuttingDown) return;
|
|
3246
3927
|
shuttingDown = true;
|
|
3247
3928
|
heartbeat.stop();
|
|
3929
|
+
resourceSampler.stop();
|
|
3930
|
+
if (runtimePublisher) clearInterval(runtimePublisher);
|
|
3931
|
+
if (updateCheckTimer) clearInterval(updateCheckTimer);
|
|
3248
3932
|
try {
|
|
3249
3933
|
if (stateService.getCurrent() === "online") {
|
|
3250
3934
|
await stateService.transition("draining", { reason });
|
|
3251
3935
|
}
|
|
3252
|
-
while (
|
|
3936
|
+
while (claimHandler.getActiveSessionCount() > 0) {
|
|
3253
3937
|
await new Promise((r) => setTimeout(r, 200));
|
|
3254
3938
|
}
|
|
3255
3939
|
await stateService.transition("stopping", { reason });
|
|
@@ -3298,8 +3982,41 @@ async function daemonStart() {
|
|
|
3298
3982
|
process.exit(1);
|
|
3299
3983
|
}
|
|
3300
3984
|
console.log("Connected.");
|
|
3985
|
+
claimHandler.attachToSocket(comms.inner());
|
|
3301
3986
|
await stateService.transition("online");
|
|
3302
3987
|
heartbeat.start();
|
|
3988
|
+
resourceSampler.start();
|
|
3989
|
+
const publishSnapshot = () => {
|
|
3990
|
+
void tokenRefresher.maybeRefresh();
|
|
3991
|
+
const tokenStatus = tokenObserver.emitIfNeeded();
|
|
3992
|
+
const disconnectedMs = comms.disconnectedForMs();
|
|
3993
|
+
const resourceSnap = resourceSampler.snapshot();
|
|
3994
|
+
stateService.publishRuntime({
|
|
3995
|
+
snapshotAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3996
|
+
activeSessions: claimHandler.getActiveSessionCount(),
|
|
3997
|
+
maxConcurrentSessions: claimHandler.getMaxConcurrentSessions(),
|
|
3998
|
+
socketConnected: comms.isConnected(),
|
|
3999
|
+
...disconnectedMs != null ? { socketDisconnectedForMs: disconnectedMs } : {},
|
|
4000
|
+
...resourceSnap.sampleCount > 0 ? {
|
|
4001
|
+
cpuPercent: resourceSnap.cpuPercent,
|
|
4002
|
+
memoryRssBytes: resourceSnap.memoryRssBytes
|
|
4003
|
+
} : {},
|
|
4004
|
+
updateInfo: updateChecker.info(),
|
|
4005
|
+
tokenStatus: tokenStatus.state === "unknown" ? { state: "unknown" } : tokenStatus.state === "expired" ? { state: "expired", expiresAt: tokenStatus.expiresAt } : {
|
|
4006
|
+
state: tokenStatus.state,
|
|
4007
|
+
daysRemaining: tokenStatus.daysRemaining,
|
|
4008
|
+
expiresAt: tokenStatus.expiresAt
|
|
4009
|
+
}
|
|
4010
|
+
});
|
|
4011
|
+
};
|
|
4012
|
+
publishSnapshot();
|
|
4013
|
+
runtimePublisher = setInterval(publishSnapshot, 3e4);
|
|
4014
|
+
runtimePublisher.unref?.();
|
|
4015
|
+
void updateChecker.check();
|
|
4016
|
+
updateCheckTimer = setInterval(() => {
|
|
4017
|
+
void updateChecker.check();
|
|
4018
|
+
}, 60 * 60 * 1e3);
|
|
4019
|
+
updateCheckTimer.unref?.();
|
|
3303
4020
|
console.log(
|
|
3304
4021
|
`Daemon online. machineId=${creds.machineId} pid=${process.pid}. Press Ctrl-C to drain.`
|
|
3305
4022
|
);
|
|
@@ -3324,6 +4041,64 @@ async function daemonStatus() {
|
|
|
3324
4041
|
if (persisted.pid) console.log(`PID: ${persisted.pid}`);
|
|
3325
4042
|
console.log(`Entered state: ${persisted.enteredAt}`);
|
|
3326
4043
|
if (persisted.reason) console.log(`Reason: ${persisted.reason}`);
|
|
4044
|
+
if (persisted.runtime) {
|
|
4045
|
+
const rt = persisted.runtime;
|
|
4046
|
+
console.log(
|
|
4047
|
+
`Active sessions: ${rt.activeSessions} / ${rt.maxConcurrentSessions}`
|
|
4048
|
+
);
|
|
4049
|
+
const socketStatus = rt.socketConnected ? "connected" : rt.socketDisconnectedForMs != null ? `disconnected ${Math.round(rt.socketDisconnectedForMs / 1e3)}s` : "disconnected";
|
|
4050
|
+
console.log(`Comms socket: ${socketStatus}`);
|
|
4051
|
+
if (rt.cpuPercent != null) {
|
|
4052
|
+
console.log(`CPU (60s avg): ${rt.cpuPercent.toFixed(1)}%`);
|
|
4053
|
+
}
|
|
4054
|
+
if (rt.memoryRssBytes != null) {
|
|
4055
|
+
const mb = rt.memoryRssBytes / (1024 * 1024);
|
|
4056
|
+
console.log(`Memory (RSS): ${mb.toFixed(1)} MB`);
|
|
4057
|
+
}
|
|
4058
|
+
if (rt.updateInfo) {
|
|
4059
|
+
const u = rt.updateInfo;
|
|
4060
|
+
const versionLine = u.updateAvailable ? `${u.currentVersion} \u2192 ${u.latestVersion} available (run: onklave daemon update)` : `${u.currentVersion}${u.latestVersion ? " (latest)" : ""}`;
|
|
4061
|
+
console.log(`Daemon version: ${versionLine}`);
|
|
4062
|
+
}
|
|
4063
|
+
if (rt.tokenStatus) {
|
|
4064
|
+
if (rt.tokenStatus.state === "expired") {
|
|
4065
|
+
console.log(`Device token: EXPIRED (${rt.tokenStatus.expiresAt})`);
|
|
4066
|
+
} else if (rt.tokenStatus.state === "warning") {
|
|
4067
|
+
console.log(
|
|
4068
|
+
`Device token: \u26A0 ${rt.tokenStatus.daysRemaining} days remaining (${rt.tokenStatus.expiresAt})`
|
|
4069
|
+
);
|
|
4070
|
+
} else if (rt.tokenStatus.state === "ok") {
|
|
4071
|
+
console.log(
|
|
4072
|
+
`Device token: ${rt.tokenStatus.daysRemaining} days remaining`
|
|
4073
|
+
);
|
|
4074
|
+
} else {
|
|
4075
|
+
console.log("Device token: expiry unknown \u2014 re-register to refresh");
|
|
4076
|
+
}
|
|
4077
|
+
}
|
|
4078
|
+
console.log(`Snapshot age: ${snapshotAge(rt.snapshotAt)}`);
|
|
4079
|
+
}
|
|
4080
|
+
}
|
|
4081
|
+
function snapshotAge(iso) {
|
|
4082
|
+
const ms = Date.now() - new Date(iso).getTime();
|
|
4083
|
+
if (ms < 0) return "just now";
|
|
4084
|
+
if (ms < 6e4) return `${Math.round(ms / 1e3)}s ago`;
|
|
4085
|
+
if (ms < 36e5) return `${Math.round(ms / 6e4)}m ago`;
|
|
4086
|
+
return `${Math.round(ms / 36e5)}h ago`;
|
|
4087
|
+
}
|
|
4088
|
+
async function daemonUpdate() {
|
|
4089
|
+
console.log("To update the Onklave agent CLI, run as the install owner:");
|
|
4090
|
+
console.log("");
|
|
4091
|
+
console.log(" npm install -g @onklave/agent-cli@latest");
|
|
4092
|
+
console.log("");
|
|
4093
|
+
console.log(
|
|
4094
|
+
"Then restart the supervisor: `systemctl restart onklave-daemon` (Linux),"
|
|
4095
|
+
);
|
|
4096
|
+
console.log("`launchctl kickstart -k system/app.onklave.daemon` (macOS), or");
|
|
4097
|
+
console.log("`nssm restart OnklaveDaemon` (Windows).");
|
|
4098
|
+
console.log("");
|
|
4099
|
+
console.log(
|
|
4100
|
+
"Self-replace via npm provenance verification (spec \xA76.3) is deferred \u2014 see V6-CLI-002 Phase 7 in the kanban."
|
|
4101
|
+
);
|
|
3327
4102
|
}
|
|
3328
4103
|
async function daemonStop() {
|
|
3329
4104
|
const pidFile = defaultPidFilePath();
|
|
@@ -3380,6 +4155,25 @@ function isPidAlive(pidFile) {
|
|
|
3380
4155
|
if (pid == null) return false;
|
|
3381
4156
|
return isProcessAlive(pid);
|
|
3382
4157
|
}
|
|
4158
|
+
function readPackageVersion() {
|
|
4159
|
+
try {
|
|
4160
|
+
const candidates = [
|
|
4161
|
+
// dist layout (bin/onklave.js → ../package.json)
|
|
4162
|
+
`${__dirname}/../../package.json`,
|
|
4163
|
+
// src layout during dev
|
|
4164
|
+
`${__dirname}/../../../package.json`
|
|
4165
|
+
];
|
|
4166
|
+
for (const p of candidates) {
|
|
4167
|
+
if (fs6.existsSync(p)) {
|
|
4168
|
+
const pkg = JSON.parse(fs6.readFileSync(p, "utf8"));
|
|
4169
|
+
if (pkg.name === "@onklave/agent-cli" && pkg.version)
|
|
4170
|
+
return pkg.version;
|
|
4171
|
+
}
|
|
4172
|
+
}
|
|
4173
|
+
} catch {
|
|
4174
|
+
}
|
|
4175
|
+
return null;
|
|
4176
|
+
}
|
|
3383
4177
|
function isProcessAlive(pid) {
|
|
3384
4178
|
try {
|
|
3385
4179
|
process.kill(pid, 0);
|