@hivemagnus/magnus-server 1.0.2 → 1.1.0
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/.env.example +30 -0
- package/README.md +22 -3
- package/agent-version.json +3 -1
- package/dist/cli/agents.js +111 -0
- package/dist/cli/args.js +19 -4
- package/dist/cli/auto-migrate.js +85 -0
- package/dist/cli/env-catalog.js +17 -0
- package/dist/cli/migrations.js +117 -0
- package/dist/cli/preflight.js +5 -14
- package/dist/cli/upgrade.js +55 -0
- package/dist/cli.js +121 -7
- package/dist/config/env.js +26 -0
- package/dist/config/features.js +22 -0
- package/dist/config/route-aliases.js +73 -0
- package/dist/index.js +13 -5
- package/dist/openapi/spec.js +72 -31
- package/dist/routes/agent-upgrades.js +42 -0
- package/dist/routes/heartbeat.js +8 -1
- package/dist/routes/self-update.js +19 -0
- package/dist/routes/system-updates.js +27 -0
- package/dist/sdk/node-api-client.js +4 -3
- package/dist/security/approval-assertion.js +23 -1
- package/dist/security/native-completion-registry.js +2 -0
- package/dist/server.js +5 -0
- package/dist/services/agent-upgrade-service.js +293 -0
- package/dist/services/agent-version-warnings.js +31 -0
- package/dist/services/self-update-service.js +11 -8
- package/dist/services/update-check-service.js +178 -0
- package/dist/services/webhook-service.js +13 -10
- package/dist/services/webhook-types.js +29 -0
- package/dist/utils/runtime-file.js +23 -0
- package/package.json +3 -3
package/dist/cli.js
CHANGED
|
@@ -4,12 +4,13 @@
|
|
|
4
4
|
// named on the command line have been applied.
|
|
5
5
|
import { spawn } from 'node:child_process';
|
|
6
6
|
import { existsSync } from 'node:fs';
|
|
7
|
-
import { createRequire } from 'node:module';
|
|
8
7
|
import { dirname, join, resolve } from 'node:path';
|
|
9
8
|
import { fileURLToPath } from 'node:url';
|
|
10
9
|
import { parseArgs, USAGE, UsageError } from './cli/args.js';
|
|
11
10
|
import { buildEnvSchemaDocument, renderEnvTable } from './cli/env-catalog.js';
|
|
12
11
|
const PACKAGE_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
12
|
+
/** Node reads NODE_EXTRA_CA_CERTS only at startup; an env file that sets it needs a fresh process. */
|
|
13
|
+
const STARTUP_EXTRA_CA = process.env['NODE_EXTRA_CA_CERTS'];
|
|
13
14
|
const MIGRATIONS_DIR = join(PACKAGE_ROOT, 'prisma', 'migrations');
|
|
14
15
|
const PRISMA_CONFIG = join(PACKAGE_ROOT, 'prisma.config.ts');
|
|
15
16
|
class FatalError extends Error {
|
|
@@ -47,10 +48,10 @@ async function version() {
|
|
|
47
48
|
async function migrate() {
|
|
48
49
|
if (!process.env['DATABASE_URL'] && process.env['NODE_ENV'] === 'production')
|
|
49
50
|
fatal('Configuration error: DATABASE_URL is required to run migrations in production.');
|
|
51
|
+
const { prismaCliPath } = await import('./cli/migrations.js');
|
|
50
52
|
let prismaCli;
|
|
51
53
|
try {
|
|
52
|
-
|
|
53
|
-
prismaCli = join(dirname(require.resolve('prisma/package.json')), 'build', 'index.js');
|
|
54
|
+
prismaCli = prismaCliPath();
|
|
54
55
|
}
|
|
55
56
|
catch {
|
|
56
57
|
return fatal('Migration error: the prisma CLI package is not installed next to magnus-server.');
|
|
@@ -75,7 +76,7 @@ async function migrate() {
|
|
|
75
76
|
await disconnectDb();
|
|
76
77
|
}
|
|
77
78
|
}
|
|
78
|
-
async function commandCheck() {
|
|
79
|
+
async function commandCheck(format) {
|
|
79
80
|
const { listMissingSecrets } = await import('./utils/env.js');
|
|
80
81
|
const { loadEnv } = await import('./config/env.js');
|
|
81
82
|
const { featureFlags } = await import('./config/features.js');
|
|
@@ -104,11 +105,19 @@ async function commandCheck() {
|
|
|
104
105
|
if (databaseUrl)
|
|
105
106
|
results.push(...await checkDatabase(databaseUrl, MIGRATIONS_DIR));
|
|
106
107
|
results.push(await checkRedis(process.env['REDIS_URL'] ?? 'redis://127.0.0.1:6379'));
|
|
108
|
+
const { checkForUpdate, updateResult } = await import('./services/update-check-service.js');
|
|
109
|
+
const update = await checkForUpdate();
|
|
110
|
+
results.push(updateResult(update));
|
|
111
|
+
const features = featureFlags();
|
|
112
|
+
const failures = results.filter(result => result.status === 'fail').length;
|
|
113
|
+
if (format === 'json') {
|
|
114
|
+
console.log(JSON.stringify({ version: await version(), ok: failures === 0, results, integrations: features, update }, null, 2));
|
|
115
|
+
return failures === 0 ? 0 : 1;
|
|
116
|
+
}
|
|
107
117
|
const symbol = { pass: '✓', warn: '!', fail: '✗' };
|
|
108
118
|
console.log(`magnus-server ${await version()} configuration check`);
|
|
109
119
|
for (const result of results)
|
|
110
120
|
console.log(` ${symbol[result.status]} ${result.name.padEnd(12)} ${result.message}`);
|
|
111
|
-
const features = featureFlags();
|
|
112
121
|
const onOff = (value) => (value ? 'on' : 'off');
|
|
113
122
|
console.log('Integrations:');
|
|
114
123
|
console.log(` Hive IAM ${onOff(features.iam)}`);
|
|
@@ -119,13 +128,94 @@ async function commandCheck() {
|
|
|
119
128
|
console.log(` Hermes registry ${onOff(features.hermesRegistry)}`);
|
|
120
129
|
console.log(` Resource leases ${onOff(features.resourceLeases)}`);
|
|
121
130
|
console.log(` Agent discovery ${onOff(features.agentDiscovery)}`);
|
|
122
|
-
const failures = results.filter(result => result.status === 'fail').length;
|
|
123
131
|
console.log(failures === 0 ? 'Result: OK' : `Result: ${failures} problem(s) found`);
|
|
124
132
|
return failures === 0 ? 0 : 1;
|
|
125
133
|
}
|
|
134
|
+
/** Applied and pending migrations, with impact notes (`--format json` for tooling). */
|
|
135
|
+
async function commandMigrations(format) {
|
|
136
|
+
const { resolveDatabaseUrl } = await import('./db/client.js');
|
|
137
|
+
const { readMigrationState } = await import('./cli/migrations.js');
|
|
138
|
+
const pg = (await import('pg')).default;
|
|
139
|
+
const client = new pg.Client({ connectionString: resolveDatabaseUrl(), connectionTimeoutMillis: 10_000 });
|
|
140
|
+
await client.connect();
|
|
141
|
+
try {
|
|
142
|
+
const state = await readMigrationState(client, MIGRATIONS_DIR);
|
|
143
|
+
if (format === 'json') {
|
|
144
|
+
console.log(JSON.stringify({ version: await version(), packaged: state.packaged, applied: [...state.applied].sort(), pending: state.pending, fresh: state.fresh }, null, 2));
|
|
145
|
+
}
|
|
146
|
+
else {
|
|
147
|
+
for (const name of state.packaged) {
|
|
148
|
+
const pending = state.pending.find(migration => migration.name === name);
|
|
149
|
+
console.log(` ${pending ? '…' : '✓'} ${name}${pending ? ` (pending, ${pending.impact}${pending.note ? `: ${pending.note}` : ''})` : ''}`);
|
|
150
|
+
}
|
|
151
|
+
console.log(state.pending.length === 0 ? 'All migrations applied.' : `${state.pending.length} pending; run \`magnus-server migrate\`.`);
|
|
152
|
+
}
|
|
153
|
+
return 0;
|
|
154
|
+
}
|
|
155
|
+
finally {
|
|
156
|
+
await client.end().catch(() => undefined);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
/** `pg_dump -Fc` into MAGNUS_BACKUP_DIR; prints JSON with the file and restore command. */
|
|
160
|
+
async function commandBackup(label) {
|
|
161
|
+
const { resolveDatabaseUrl } = await import('./db/client.js');
|
|
162
|
+
const { backupDatabase } = await import('./cli/migrations.js');
|
|
163
|
+
const { defaultBackupDir } = await import('./cli/auto-migrate.js');
|
|
164
|
+
const { magnusHome } = await import('./utils/magnus-home.js');
|
|
165
|
+
const pg = (await import('pg')).default;
|
|
166
|
+
const databaseUrl = resolveDatabaseUrl();
|
|
167
|
+
const client = new pg.Client({ connectionString: databaseUrl, connectionTimeoutMillis: 10_000 });
|
|
168
|
+
await client.connect();
|
|
169
|
+
try {
|
|
170
|
+
const result = await backupDatabase({
|
|
171
|
+
databaseUrl, client, backupDir: defaultBackupDir(process.env, magnusHome()),
|
|
172
|
+
label: label ?? `manual-${await version()}`, pgDump: process.env['MAGNUS_PG_DUMP'],
|
|
173
|
+
});
|
|
174
|
+
console.log(JSON.stringify(result));
|
|
175
|
+
return 0;
|
|
176
|
+
}
|
|
177
|
+
finally {
|
|
178
|
+
await client.end().catch(() => undefined);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
/** `MAGNUS_AUTO_MIGRATE=true`: back up and migrate under an advisory lock before listening. */
|
|
182
|
+
async function startupAutoMigrate() {
|
|
183
|
+
const { upgradeSettings } = await import('./config/features.js');
|
|
184
|
+
const settings = upgradeSettings();
|
|
185
|
+
if (!settings.autoMigrate)
|
|
186
|
+
return;
|
|
187
|
+
const { resolveDatabaseUrl } = await import('./db/client.js');
|
|
188
|
+
const { autoMigrate, defaultBackupDir } = await import('./cli/auto-migrate.js');
|
|
189
|
+
const { magnusHome } = await import('./utils/magnus-home.js');
|
|
190
|
+
try {
|
|
191
|
+
await autoMigrate({
|
|
192
|
+
databaseUrl: resolveDatabaseUrl(),
|
|
193
|
+
migrationsDir: MIGRATIONS_DIR,
|
|
194
|
+
backupDir: defaultBackupDir(process.env, magnusHome()),
|
|
195
|
+
version: await version(),
|
|
196
|
+
settings,
|
|
197
|
+
pgDump: process.env['MAGNUS_PG_DUMP'],
|
|
198
|
+
migrate,
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
catch (error) {
|
|
202
|
+
if (error instanceof FatalError)
|
|
203
|
+
throw error;
|
|
204
|
+
fatal(error instanceof Error ? error.message : String(error));
|
|
205
|
+
}
|
|
206
|
+
}
|
|
126
207
|
async function commandStart(options) {
|
|
208
|
+
// Without --env-file the server reads its package-level .env; apply it here
|
|
209
|
+
// too so startup switches such as MAGNUS_AUTO_MIGRATE see the same values.
|
|
210
|
+
const packageEnv = join(PACKAGE_ROOT, '.env');
|
|
211
|
+
if (!process.env['MAGNUS_ENV_FILE'] && existsSync(packageEnv)) {
|
|
212
|
+
process.loadEnvFile(packageEnv);
|
|
213
|
+
dropEmptyValues();
|
|
214
|
+
}
|
|
127
215
|
if (options.migrate)
|
|
128
216
|
await migrate();
|
|
217
|
+
else
|
|
218
|
+
await startupAutoMigrate();
|
|
129
219
|
await import('./index.js');
|
|
130
220
|
}
|
|
131
221
|
async function run(argv) {
|
|
@@ -146,7 +236,31 @@ async function run(argv) {
|
|
|
146
236
|
return 0;
|
|
147
237
|
case 'check':
|
|
148
238
|
loadEnvFiles(options.envFiles);
|
|
149
|
-
return commandCheck();
|
|
239
|
+
return commandCheck(options.formatGiven ? options.format : 'table');
|
|
240
|
+
case 'migrations':
|
|
241
|
+
loadEnvFiles(options.envFiles);
|
|
242
|
+
return commandMigrations(options.formatGiven ? options.format : 'table');
|
|
243
|
+
case 'backup':
|
|
244
|
+
loadEnvFiles(options.envFiles);
|
|
245
|
+
return commandBackup(options.label);
|
|
246
|
+
case 'upgrade':
|
|
247
|
+
case 'rollback': {
|
|
248
|
+
const { delegateToProject } = await import('./cli/upgrade.js');
|
|
249
|
+
return delegateToProject(options.command, options.passthrough, PACKAGE_ROOT);
|
|
250
|
+
}
|
|
251
|
+
case 'agents': {
|
|
252
|
+
loadEnvFiles(options.envFiles);
|
|
253
|
+
// The command calls agents over TLS: trust the same extra CA as the service.
|
|
254
|
+
if (process.env['NODE_EXTRA_CA_CERTS'] && process.env['NODE_EXTRA_CA_CERTS'] !== STARTUP_EXTRA_CA) {
|
|
255
|
+
return new Promise((resolvePromise, reject) => {
|
|
256
|
+
const child = spawn(process.execPath, process.argv.slice(1), { stdio: 'inherit', env: process.env });
|
|
257
|
+
child.on('error', reject);
|
|
258
|
+
child.on('close', code => resolvePromise(code ?? 1));
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
const { commandAgents } = await import('./cli/agents.js');
|
|
262
|
+
return commandAgents(options.passthrough);
|
|
263
|
+
}
|
|
150
264
|
case 'migrate':
|
|
151
265
|
loadEnvFiles(options.envFiles);
|
|
152
266
|
await migrate();
|
package/dist/config/env.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
+
import { parseRouteAliases } from './route-aliases.js';
|
|
4
|
+
import { webhookTypeAliases } from '../services/webhook-types.js';
|
|
3
5
|
// ─── Shared environment schema — validated with Zod ───
|
|
4
6
|
const portSchema = z.coerce.number().int().min(1).max(65535);
|
|
5
7
|
const booleanEnvSchema = z.enum(['true', 'false']).default('false').transform((value) => value === 'true');
|
|
@@ -48,6 +50,10 @@ export const envSchema = z.object({
|
|
|
48
50
|
APPROVAL_TELEGRAM_ACTOR_SHA256: z.string().regex(/^[a-f0-9]{64}$/).optional(),
|
|
49
51
|
APPROVAL_IPHONE_ACTOR_SHA256: z.string().regex(/^[a-f0-9]{64}$/).optional(),
|
|
50
52
|
MAGNUS_APPROVAL_BRIDGE_CONSUMER: z.string().regex(/^[a-z0-9][a-z0-9._-]{0,63}$/i).optional(),
|
|
53
|
+
// Accepted approval-assertion signature domain prefixes (comma-separated); first is preferred.
|
|
54
|
+
MAGNUS_APPROVAL_ASSERTION_DOMAINS: z.string()
|
|
55
|
+
.regex(/^\s*[a-z0-9](?:[a-z0-9.-]{0,98}[a-z0-9])?\s*(?:,\s*[a-z0-9](?:[a-z0-9.-]{0,98}[a-z0-9])?\s*)*$/, 'must list domain prefixes made of a-z, 0-9, "." and "-", separated by commas')
|
|
56
|
+
.optional(),
|
|
51
57
|
// Security Fusion: off (default) | local (in-process) | remote (external security console).
|
|
52
58
|
SECURITY_FUSION_MODE: z.enum(['off', 'local', 'remote']).default('off'),
|
|
53
59
|
SECURITY_FUSION_API_URL: z.string().url().optional(),
|
|
@@ -151,7 +157,27 @@ export const envSchema = z.object({
|
|
|
151
157
|
HERMES_HIVEMEM_WORKSPACE_ID: z.string().optional(),
|
|
152
158
|
HERMES_HIVEMEM_DEFAULT_SCOPE: z.enum(['private', 'shared']).optional(),
|
|
153
159
|
HERMES_OPENAI_RUNTIME: z.enum(['auto', 'codex_app_server']).optional(),
|
|
160
|
+
// Optional legacy-path -> current-path prefix map for older clients (see config/route-aliases.ts).
|
|
161
|
+
MAGNUS_ROUTE_ALIASES_JSON: z.string().optional(),
|
|
162
|
+
// Optional legacy webhook type names: comma-separated legacy=format pairs.
|
|
163
|
+
MAGNUS_WEBHOOK_TYPE_ALIASES: z.string()
|
|
164
|
+
.regex(/^\s*(?:[a-z][a-z0-9_-]{0,31}\s*=\s*(?:generic|slack|discord|mattermost)\s*(?:,\s*|$))*$/, 'must list legacy=format pairs, where format is generic, slack, discord or mattermost')
|
|
165
|
+
.optional(),
|
|
154
166
|
}).superRefine((env, ctx) => {
|
|
167
|
+
try {
|
|
168
|
+
webhookTypeAliases(env.MAGNUS_WEBHOOK_TYPE_ALIASES);
|
|
169
|
+
}
|
|
170
|
+
catch (error) {
|
|
171
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, path: ['MAGNUS_WEBHOOK_TYPE_ALIASES'],
|
|
172
|
+
message: error instanceof Error ? error.message : 'MAGNUS_WEBHOOK_TYPE_ALIASES is invalid' });
|
|
173
|
+
}
|
|
174
|
+
try {
|
|
175
|
+
parseRouteAliases(env.MAGNUS_ROUTE_ALIASES_JSON);
|
|
176
|
+
}
|
|
177
|
+
catch (error) {
|
|
178
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, path: ['MAGNUS_ROUTE_ALIASES_JSON'],
|
|
179
|
+
message: error instanceof Error ? error.message : 'MAGNUS_ROUTE_ALIASES_JSON is invalid' });
|
|
180
|
+
}
|
|
155
181
|
if (env.HEALTH_SNAPSHOT_FULL_FIDELITY_HOURS > env.HEALTH_SNAPSHOT_RETENTION_DAYS * 24) {
|
|
156
182
|
ctx.addIssue({
|
|
157
183
|
code: z.ZodIssueCode.custom,
|
package/dist/config/features.js
CHANGED
|
@@ -13,4 +13,26 @@ export function featureFlags(env = process.env) {
|
|
|
13
13
|
agentDiscovery: truthy(env['ENABLE_AGENT_DISCOVERY']),
|
|
14
14
|
};
|
|
15
15
|
}
|
|
16
|
+
/** Boolean switch with an explicit default: only `true`/`false` (any case) override it. */
|
|
17
|
+
export function envSwitch(value, fallback) {
|
|
18
|
+
const normalized = (value ?? '').trim().toLowerCase();
|
|
19
|
+
if (normalized === 'true')
|
|
20
|
+
return true;
|
|
21
|
+
if (normalized === 'false')
|
|
22
|
+
return false;
|
|
23
|
+
return fallback;
|
|
24
|
+
}
|
|
25
|
+
export function upgradeSettings(env = process.env) {
|
|
26
|
+
const concurrency = Number.parseInt(env['MAGNUS_AGENT_UPGRADE_CONCURRENCY'] ?? '', 10);
|
|
27
|
+
return {
|
|
28
|
+
autoMigrate: envSwitch(env['MAGNUS_AUTO_MIGRATE'], false),
|
|
29
|
+
autoMigrateBackup: envSwitch(env['MAGNUS_AUTO_MIGRATE_BACKUP'], true),
|
|
30
|
+
autoMigrateAllowSlow: envSwitch(env['MAGNUS_AUTO_MIGRATE_ALLOW_SLOW'], false),
|
|
31
|
+
updateCheck: envSwitch(env['MAGNUS_UPDATE_CHECK'], true),
|
|
32
|
+
updateRegistry: (env['MAGNUS_UPDATE_REGISTRY']?.trim() || 'https://registry.npmjs.org').replace(/\/+$/, ''),
|
|
33
|
+
agentUpgrades: envSwitch(env['MAGNUS_AGENT_UPGRADES'], true),
|
|
34
|
+
agentUpgradeConcurrency: Number.isInteger(concurrency) && concurrency > 0 ? Math.min(concurrency, 50) : 5,
|
|
35
|
+
legacyGitSelfUpdate: envSwitch(env['MAGNUS_LEGACY_GIT_SELF_UPDATE'], false),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
16
38
|
//# sourceMappingURL=features.js.map
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Optional request path aliases for clients built against older route names.
|
|
3
|
+
*
|
|
4
|
+
* `MAGNUS_ROUTE_ALIASES_JSON` is a JSON object that maps a legacy path prefix
|
|
5
|
+
* to the current one, for example
|
|
6
|
+
* `{"/api/v1/old/prefix": "/api/v1/new/prefix"}`. A request whose path equals a
|
|
7
|
+
* legacy prefix, or continues it after a `/`, is rewritten before routing, so
|
|
8
|
+
* authentication, endpoint scopes, rate limits and audit all see the current
|
|
9
|
+
* path. Unset (the default) means no aliases.
|
|
10
|
+
*/
|
|
11
|
+
const segment = '[A-Za-z0-9._~-]+';
|
|
12
|
+
const aliasPath = new RegExp(`^/api/v1(?:/${segment})+$`);
|
|
13
|
+
function validPath(path) {
|
|
14
|
+
return aliasPath.test(path) && !path.split('/').some(part => part === '.' || part === '..');
|
|
15
|
+
}
|
|
16
|
+
/** Parse and validate the alias map; throws a descriptive error when it is malformed. */
|
|
17
|
+
export function parseRouteAliases(raw) {
|
|
18
|
+
if (raw === undefined || raw.trim() === '')
|
|
19
|
+
return [];
|
|
20
|
+
let value;
|
|
21
|
+
try {
|
|
22
|
+
value = JSON.parse(raw);
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
throw new Error('MAGNUS_ROUTE_ALIASES_JSON must be a JSON object');
|
|
26
|
+
}
|
|
27
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
28
|
+
throw new Error('MAGNUS_ROUTE_ALIASES_JSON must be a JSON object');
|
|
29
|
+
const entries = Object.entries(value);
|
|
30
|
+
if (entries.length > 64)
|
|
31
|
+
throw new Error('MAGNUS_ROUTE_ALIASES_JSON may define at most 64 aliases');
|
|
32
|
+
const aliases = [];
|
|
33
|
+
for (const [from, to] of entries) {
|
|
34
|
+
if (typeof to !== 'string' || !validPath(from) || !validPath(to))
|
|
35
|
+
throw new Error('MAGNUS_ROUTE_ALIASES_JSON keys and values must be /api/v1/... paths without "." or ".." segments');
|
|
36
|
+
if (from === to)
|
|
37
|
+
throw new Error('MAGNUS_ROUTE_ALIASES_JSON maps a path to itself');
|
|
38
|
+
aliases.push({ from, to });
|
|
39
|
+
}
|
|
40
|
+
for (const { to } of aliases)
|
|
41
|
+
if (aliases.some(alias => to === alias.from || to.startsWith(`${alias.from}/`)))
|
|
42
|
+
throw new Error('MAGNUS_ROUTE_ALIASES_JSON targets must not be aliases themselves');
|
|
43
|
+
// Longest prefix first, so a more specific alias wins.
|
|
44
|
+
return aliases.sort((a, b) => b.from.length - a.from.length);
|
|
45
|
+
}
|
|
46
|
+
/** The current path for `path`, or null when no alias applies. */
|
|
47
|
+
export function resolveRouteAlias(path, aliases) {
|
|
48
|
+
for (const { from, to } of aliases) {
|
|
49
|
+
if (path === from)
|
|
50
|
+
return to;
|
|
51
|
+
if (path.startsWith(`${from}/`))
|
|
52
|
+
return to + path.slice(from.length);
|
|
53
|
+
}
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
/** Wrap a fetch handler so legacy paths reach their current routes. Returns `fetch` unchanged without aliases. */
|
|
57
|
+
export function withRouteAliases(fetch, aliases) {
|
|
58
|
+
if (!aliases.length)
|
|
59
|
+
return fetch;
|
|
60
|
+
return (request, env, context) => {
|
|
61
|
+
const url = new URL(request.url);
|
|
62
|
+
const target = resolveRouteAlias(url.pathname, aliases);
|
|
63
|
+
if (target === null)
|
|
64
|
+
return fetch(request, env, context);
|
|
65
|
+
url.pathname = target;
|
|
66
|
+
const init = {
|
|
67
|
+
method: request.method, headers: request.headers, redirect: request.redirect, signal: request.signal,
|
|
68
|
+
...(request.body ? { body: request.body, duplex: 'half' } : {}),
|
|
69
|
+
};
|
|
70
|
+
return fetch(new Request(url, init), env, context);
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
//# sourceMappingURL=route-aliases.js.map
|
package/dist/index.js
CHANGED
|
@@ -18,6 +18,8 @@ const { securityFusionLocal, securityFusionMode, startSecurityExporter, stopSecu
|
|
|
18
18
|
const { featureFlags } = await import('./config/features.js');
|
|
19
19
|
const { serve } = await import('@hono/node-server');
|
|
20
20
|
const { app } = await import('./server.js');
|
|
21
|
+
const { parseRouteAliases, withRouteAliases } = await import('./config/route-aliases.js');
|
|
22
|
+
const handler = withRouteAliases(app.fetch, parseRouteAliases(process.env['MAGNUS_ROUTE_ALIASES_JSON']));
|
|
21
23
|
const { startRolloutReconciler, stopRolloutReconciler } = await import('./services/rollout-service.js');
|
|
22
24
|
const { startApplicationProbes, stopApplicationProbes } = await import('./services/application-probes.js');
|
|
23
25
|
const { startNodeIssueMonitor } = await import('./services/node-issue-monitor.js');
|
|
@@ -31,6 +33,7 @@ const { startSecurityFusionRetention, stopSecurityFusionRetention } = await impo
|
|
|
31
33
|
const { reconcileSecurityFusionEvidence } = await import('./services/security-fusion-evidence-integrity.js');
|
|
32
34
|
const { startSecurityFusionOutboxDispatcher, stopSecurityFusionOutboxDispatcher, } = await import('./services/security-fusion-outbox.js');
|
|
33
35
|
const { startCommandWorker, closeCommandQueue } = await import('./queue/workers.js');
|
|
36
|
+
const { startUpdateCheck, stopUpdateCheck } = await import('./services/update-check-service.js');
|
|
34
37
|
const { startWebhookWorker, closeWebhookQueue } = await import('./services/webhook-service.js');
|
|
35
38
|
const { setupTerminalWebSocket } = await import('./routes/terminal.js');
|
|
36
39
|
const { disconnectRedis } = await import('./queue/redis.js');
|
|
@@ -49,8 +52,11 @@ if (securityFusionLocal()) {
|
|
|
49
52
|
+ `updated=${evidenceReconciliation.updated} quarantined=${evidenceReconciliation.quarantined} `
|
|
50
53
|
+ `mismatched_cases=${evidenceReconciliation.mismatchedCases}`);
|
|
51
54
|
}
|
|
55
|
+
const { writeRuntimeFile } = await import('./utils/runtime-file.js');
|
|
56
|
+
const { CONTROL_API_VERSION } = await import('./config/version.js');
|
|
52
57
|
/** Background services shared by the TLS and development listeners. */
|
|
53
|
-
function startBackgroundServices() {
|
|
58
|
+
function startBackgroundServices(listenPort) {
|
|
59
|
+
writeRuntimeFile({ version: CONTROL_API_VERSION, port: listenPort });
|
|
54
60
|
startNodeIssueMonitor();
|
|
55
61
|
startApplicationProbes();
|
|
56
62
|
startLogRetention();
|
|
@@ -65,6 +71,7 @@ function startBackgroundServices() {
|
|
|
65
71
|
startRolloutReconciler();
|
|
66
72
|
startWebhookWorker();
|
|
67
73
|
startCodexFleetMonitor();
|
|
74
|
+
startUpdateCheck();
|
|
68
75
|
if (features.hermesRegistry) {
|
|
69
76
|
startExtensionSourceMonitor();
|
|
70
77
|
void importPackageSkills().then(result => console.log(`[hermes-registry] catalog import: ${JSON.stringify(result)}`))
|
|
@@ -89,7 +96,7 @@ const listener = resolveListenerSecurity({
|
|
|
89
96
|
});
|
|
90
97
|
const server = listener.useTls
|
|
91
98
|
? serve({
|
|
92
|
-
fetch:
|
|
99
|
+
fetch: handler,
|
|
93
100
|
port,
|
|
94
101
|
hostname: listener.hostname,
|
|
95
102
|
createServer,
|
|
@@ -99,11 +106,11 @@ const server = listener.useTls
|
|
|
99
106
|
},
|
|
100
107
|
}, (info) => {
|
|
101
108
|
console.log(`🚀 Magnus server listening on https://${listener.hostname}:${info.port}`);
|
|
102
|
-
startBackgroundServices();
|
|
109
|
+
startBackgroundServices(info.port);
|
|
103
110
|
})
|
|
104
|
-
: serve({ fetch:
|
|
111
|
+
: serve({ fetch: handler, port, hostname: listener.hostname }, (info) => {
|
|
105
112
|
console.log(`🚀 Magnus server listening on http://${listener.hostname}:${info.port} (development opt-in)`);
|
|
106
|
-
startBackgroundServices();
|
|
113
|
+
startBackgroundServices(info.port);
|
|
107
114
|
});
|
|
108
115
|
const terminalWss = setupTerminalWebSocket(server);
|
|
109
116
|
// ── Graceful shutdown ──
|
|
@@ -131,6 +138,7 @@ async function shutdown(signal) {
|
|
|
131
138
|
stopSecurityExporter();
|
|
132
139
|
stopSecurityFusionOutboxDispatcher();
|
|
133
140
|
stopCodexFleetMonitor();
|
|
141
|
+
stopUpdateCheck();
|
|
134
142
|
stopExtensionSourceMonitor();
|
|
135
143
|
for (const client of terminalWss.clients)
|
|
136
144
|
client.terminate();
|
package/dist/openapi/spec.js
CHANGED
|
@@ -2011,36 +2011,6 @@ const paths = {
|
|
|
2011
2011
|
},
|
|
2012
2012
|
},
|
|
2013
2013
|
},
|
|
2014
|
-
'/deploy/upgrade/{nodeId}': {
|
|
2015
|
-
post: {
|
|
2016
|
-
tags: ['Deploy'],
|
|
2017
|
-
summary: 'Upgrade agent',
|
|
2018
|
-
description: 'Upgrade the agent on a node to the current expected version. Runs asynchronously.',
|
|
2019
|
-
security: apiSecretAuth,
|
|
2020
|
-
parameters: [pp('nodeId', 'Node ID')],
|
|
2021
|
-
responses: {
|
|
2022
|
-
202: success('Upgrade started'),
|
|
2023
|
-
401: err401,
|
|
2024
|
-
403: err403,
|
|
2025
|
-
404: err404,
|
|
2026
|
-
},
|
|
2027
|
-
},
|
|
2028
|
-
},
|
|
2029
|
-
'/deploy/version': {
|
|
2030
|
-
get: {
|
|
2031
|
-
tags: ['Deploy'],
|
|
2032
|
-
summary: 'Expected agent version',
|
|
2033
|
-
description: 'Returns the expected agent version and deploy username.',
|
|
2034
|
-
security: apiSecretAuth,
|
|
2035
|
-
responses: {
|
|
2036
|
-
200: success('Version info', {
|
|
2037
|
-
type: 'object',
|
|
2038
|
-
properties: { agentVersion: { type: 'string' }, deployUsername: { type: 'string' } },
|
|
2039
|
-
}),
|
|
2040
|
-
401: err401,
|
|
2041
|
-
},
|
|
2042
|
-
},
|
|
2043
|
-
},
|
|
2044
2014
|
'/deploy/control-node': {
|
|
2045
2015
|
post: {
|
|
2046
2016
|
tags: ['Deploy'],
|
|
@@ -2157,6 +2127,77 @@ const paths = {
|
|
|
2157
2127
|
},
|
|
2158
2128
|
},
|
|
2159
2129
|
// ──────────────── Self-Update ────────────────
|
|
2130
|
+
'/system/updates': {
|
|
2131
|
+
get: {
|
|
2132
|
+
tags: ['Updates'],
|
|
2133
|
+
summary: 'Available releases and agent version skew',
|
|
2134
|
+
description: 'Result of the daily npm registry check (MAGNUS_UPDATE_CHECK): the newest release of the running major, a newer major if any, release notes, new required settings and slow or one-way migrations. Also lists agents that run a different version than this server ships with, and the command that fixes it.',
|
|
2135
|
+
security: apiSecretAuth,
|
|
2136
|
+
responses: { 200: success('Update status', { type: 'object', properties: {
|
|
2137
|
+
version: { type: 'string' },
|
|
2138
|
+
server: { type: 'object', properties: { enabled: { type: 'boolean' }, current: { type: 'string' }, latest: { type: ['string', 'null'] },
|
|
2139
|
+
latestMajor: { type: ['string', 'null'] }, updateAvailable: { type: 'boolean' }, command: { type: 'string' } } },
|
|
2140
|
+
agents: { type: 'object', properties: { expected: { type: 'string' }, current: { type: 'integer' },
|
|
2141
|
+
behind: { type: 'array', items: { type: 'object' } }, ahead: { type: 'array', items: { type: 'object' } },
|
|
2142
|
+
upgradesEnabled: { type: 'boolean' }, fixCommand: { type: ['string', 'null'] } } },
|
|
2143
|
+
} }), 401: err401 },
|
|
2144
|
+
},
|
|
2145
|
+
},
|
|
2146
|
+
'/system/updates/check': {
|
|
2147
|
+
post: {
|
|
2148
|
+
tags: ['Updates'],
|
|
2149
|
+
summary: 'Check the registry now',
|
|
2150
|
+
description: 'Refreshes the update check immediately (ADMIN or OPERATOR).',
|
|
2151
|
+
security: apiSecretAuth,
|
|
2152
|
+
responses: { 200: success('Update status'), 401: err401, 403: err403 },
|
|
2153
|
+
},
|
|
2154
|
+
},
|
|
2155
|
+
'/agents/upgrade/eligibility': {
|
|
2156
|
+
get: {
|
|
2157
|
+
tags: ['Agent upgrades'],
|
|
2158
|
+
summary: 'Which agents can take a version',
|
|
2159
|
+
description: 'For every node: its agent version and whether it can upgrade itself to `version` (default: the agent version this server ships with) now, or why not (offline, not set up with `magnus-agent install-service`, MAGNUS_AGENT_SELF_UPGRADE=false, another major version, already current).',
|
|
2160
|
+
security: apiSecretAuth,
|
|
2161
|
+
parameters: [qp('version', 'Target agent version (X.Y.Z)')],
|
|
2162
|
+
responses: { 200: success('Eligibility', { type: 'object', properties: {
|
|
2163
|
+
version: { type: 'string' }, upgradesEnabled: { type: 'boolean' }, disabledReason: { type: ['string', 'null'] },
|
|
2164
|
+
nodes: { type: 'array', items: { type: 'object', properties: { nodeId: { type: 'string' }, name: { type: 'string' },
|
|
2165
|
+
version: { type: 'string' }, eligible: { type: 'boolean' }, reason: { type: ['string', 'null'] } } } },
|
|
2166
|
+
} }), 400: err400, 401: err401 },
|
|
2167
|
+
},
|
|
2168
|
+
},
|
|
2169
|
+
'/agents/upgrade': {
|
|
2170
|
+
post: {
|
|
2171
|
+
tags: ['Agent upgrades'],
|
|
2172
|
+
summary: 'Upgrade agents',
|
|
2173
|
+
description: 'Starts an upgrade run (ADMIN, MAGNUS_AGENT_UPGRADES=true): one canary, then MAGNUS_AGENT_UPGRADE_CONCURRENCY agents at a time, stopping at the first agent that does not confirm. Each agent installs the exact version and npm integrity and switches back by itself if the new version does not report a heartbeat in time.',
|
|
2174
|
+
security: apiSecretAuth,
|
|
2175
|
+
requestBody: { required: false, content: { 'application/json': { schema: { type: 'object', properties: {
|
|
2176
|
+
version: { type: 'string', description: 'Agent version (default: the one this server ships with)' },
|
|
2177
|
+
integrity: { type: 'string', description: 'npm sha512 integrity (default: read from the registry)' },
|
|
2178
|
+
nodeIds: { type: 'array', items: { type: 'string' }, description: 'Only these nodes' },
|
|
2179
|
+
} } } } },
|
|
2180
|
+
responses: { 202: success('Run started'), 400: err400, 401: err401, 403: err403, 409: err409 },
|
|
2181
|
+
},
|
|
2182
|
+
},
|
|
2183
|
+
'/agents/upgrade/runs/latest': {
|
|
2184
|
+
get: {
|
|
2185
|
+
tags: ['Agent upgrades'],
|
|
2186
|
+
summary: 'Latest upgrade run',
|
|
2187
|
+
security: apiSecretAuth,
|
|
2188
|
+
responses: { 200: success('Run (or null)'), 401: err401 },
|
|
2189
|
+
},
|
|
2190
|
+
},
|
|
2191
|
+
'/agents/upgrade/runs/{id}': {
|
|
2192
|
+
get: {
|
|
2193
|
+
tags: ['Agent upgrades'],
|
|
2194
|
+
summary: 'Upgrade run status',
|
|
2195
|
+
description: 'Per-node state of a run: pending, requested, confirmed, reverted, failed or skipped.',
|
|
2196
|
+
security: apiSecretAuth,
|
|
2197
|
+
parameters: [pp('id', 'Run ID')],
|
|
2198
|
+
responses: { 200: success('Run'), 401: err401, 404: err404 },
|
|
2199
|
+
},
|
|
2200
|
+
},
|
|
2160
2201
|
'/self-update/status': {
|
|
2161
2202
|
get: {
|
|
2162
2203
|
tags: ['Self-Update'],
|
|
@@ -2214,7 +2255,7 @@ const paths = {
|
|
|
2214
2255
|
post: {
|
|
2215
2256
|
tags: ['Self-Update'],
|
|
2216
2257
|
summary: 'Apply update',
|
|
2217
|
-
description: '
|
|
2258
|
+
description: 'Source checkouts only, and only with MAGNUS_LEGACY_GIT_SELF_UPDATE=true (otherwise 410 SELF_UPDATE_RETIRED): pull latest code, run `npm ci`, build, and optionally restart the process. npm installs upgrade with `magnus upgrade`. Requires ADMIN role.',
|
|
2218
2259
|
security: apiSecretAuth,
|
|
2219
2260
|
requestBody: {
|
|
2220
2261
|
required: false,
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { Hono } from 'hono';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { authMiddleware, getUser, requireRole } from '../middleware/auth.js';
|
|
4
|
+
import { HttpError } from '../middleware/error-handler.js';
|
|
5
|
+
import { upgradeSettings } from '../config/features.js';
|
|
6
|
+
import { AGENT_VERSION } from '../config/version.js';
|
|
7
|
+
import { getUpgradeRun, latestUpgradeRun, listUpgradeCandidates, startUpgradeRun } from '../services/agent-upgrade-service.js';
|
|
8
|
+
/** /api/v1/agents/upgrade: which agents can take a version, and fleet upgrade runs. */
|
|
9
|
+
export const agentUpgradesRouter = new Hono();
|
|
10
|
+
agentUpgradesRouter.use('*', authMiddleware);
|
|
11
|
+
const version = z.string().regex(/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/);
|
|
12
|
+
agentUpgradesRouter.get('/eligibility', async (c) => {
|
|
13
|
+
const target = version.safeParse(c.req.query('version') ?? AGENT_VERSION);
|
|
14
|
+
if (!target.success)
|
|
15
|
+
throw new HttpError(400, 'INVALID_VERSION', 'version must be X.Y.Z');
|
|
16
|
+
const nodes = await listUpgradeCandidates(target.data);
|
|
17
|
+
return c.json({ success: true, data: {
|
|
18
|
+
version: target.data, serverVersion: AGENT_VERSION, upgradesEnabled: upgradeSettings().agentUpgrades,
|
|
19
|
+
disabledReason: upgradeSettings().agentUpgrades ? null : 'MAGNUS_AGENT_UPGRADES=false on the server',
|
|
20
|
+
nodes, latestRun: await latestUpgradeRun(),
|
|
21
|
+
} });
|
|
22
|
+
});
|
|
23
|
+
const startSchema = z.object({
|
|
24
|
+
version: version.optional(),
|
|
25
|
+
integrity: z.string().regex(/^sha512-[A-Za-z0-9+/]+={0,2}$/).optional(),
|
|
26
|
+
nodeIds: z.array(z.string().min(1).max(128)).max(1000).optional(),
|
|
27
|
+
}).strict();
|
|
28
|
+
agentUpgradesRouter.post('/', requireRole('ADMIN'), async (c) => {
|
|
29
|
+
const parsed = startSchema.safeParse(await c.req.json().catch(() => ({})));
|
|
30
|
+
if (!parsed.success)
|
|
31
|
+
throw new HttpError(400, 'VALIDATION_ERROR', parsed.error.issues.map(issue => issue.message).join('; '));
|
|
32
|
+
const { run } = await startUpgradeRun({ ...parsed.data, actorId: getUser(c).userId });
|
|
33
|
+
return c.json({ success: true, data: run }, 202);
|
|
34
|
+
});
|
|
35
|
+
agentUpgradesRouter.get('/runs/latest', async (c) => c.json({ success: true, data: await latestUpgradeRun() }));
|
|
36
|
+
agentUpgradesRouter.get('/runs/:id', async (c) => {
|
|
37
|
+
const run = await getUpgradeRun(c.req.param('id'));
|
|
38
|
+
if (!run)
|
|
39
|
+
throw new HttpError(404, 'NOT_FOUND', 'Upgrade run not found');
|
|
40
|
+
return c.json({ success: true, data: run });
|
|
41
|
+
});
|
|
42
|
+
//# sourceMappingURL=agent-upgrades.js.map
|
package/dist/routes/heartbeat.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { enqueueSecurityObservations, securityFusionLocal } from '../services/security-export.js';
|
|
2
|
+
import { warnOnAgentVersionMismatch } from '../services/agent-version-warnings.js';
|
|
3
|
+
import { recordSelfUpgradeReport } from '../services/agent-upgrade-service.js';
|
|
2
4
|
import { Hono } from 'hono';
|
|
3
5
|
import { z } from 'zod';
|
|
4
6
|
import { nanoid } from 'nanoid';
|
|
@@ -330,6 +332,11 @@ heartbeatRouter.post('/', async (c) => {
|
|
|
330
332
|
}) },
|
|
331
333
|
});
|
|
332
334
|
}
|
|
335
|
+
if (existing && body.selfUpgrade !== undefined) {
|
|
336
|
+
await recordSelfUpgradeReport({ id: nodeId, name: existing.name }, body).catch((err) => {
|
|
337
|
+
console.error(`[agent-upgrade] could not record the self-upgrade report of ${existing.name}:`, err instanceof Error ? err.message : err);
|
|
338
|
+
});
|
|
339
|
+
}
|
|
333
340
|
if (!fqdn && existing) {
|
|
334
341
|
// If the node's hostname already contains a dot it's likely an FQDN — skip lookup
|
|
335
342
|
if (!existing.hostname.includes('.')) {
|
|
@@ -521,7 +528,7 @@ heartbeatRouter.post('/', async (c) => {
|
|
|
521
528
|
}
|
|
522
529
|
}
|
|
523
530
|
const expectedVersion = AGENT_VERSION;
|
|
524
|
-
const updateAvailable =
|
|
531
|
+
const { updateAvailable } = warnOnAgentVersionMismatch({ id: nodeId, name: existing?.name ?? fqdn ?? nodeId }, body.agentVersion, expectedVersion);
|
|
525
532
|
return c.json({ success: true, data: { accepted: true, expectedVersion, updateAvailable } });
|
|
526
533
|
});
|
|
527
534
|
// ── Retired Redis Streams heartbeat ingress ──
|
|
@@ -6,6 +6,7 @@ import { authMiddleware, getUser, requireRole } from '../middleware/auth.js';
|
|
|
6
6
|
import { logAuditEvent } from '../services/audit-service.js';
|
|
7
7
|
import { checkForUpdates, performSelfUpdate, scheduleRestart, getUpdateStatus, } from '../services/self-update-service.js';
|
|
8
8
|
import { HttpError } from '../middleware/error-handler.js';
|
|
9
|
+
import { upgradeSettings } from '../config/features.js';
|
|
9
10
|
export const selfUpdateRouter = new Hono();
|
|
10
11
|
selfUpdateRouter.use('*', authMiddleware);
|
|
11
12
|
selfUpdateRouter.use('*', async (c, next) => {
|
|
@@ -16,6 +17,24 @@ selfUpdateRouter.use('*', async (c, next) => {
|
|
|
16
17
|
c.header(c.res.headers.has('X-Magnus-Work-Id') ? 'X-Magnus-Restart-Work-Id' : 'X-Magnus-Work-Id', c.error.workId);
|
|
17
18
|
}
|
|
18
19
|
});
|
|
20
|
+
/**
|
|
21
|
+
* The git-checkout update path (`git pull`, rebuild, restart) only works for
|
|
22
|
+
* source installs. npm installs upgrade with `magnus upgrade`, which backs up
|
|
23
|
+
* the database and reverts on failure; the old endpoints stay behind
|
|
24
|
+
* MAGNUS_LEGACY_GIT_SELF_UPDATE=true.
|
|
25
|
+
*/
|
|
26
|
+
const legacyGitSelfUpdate = async (c, next) => {
|
|
27
|
+
if (!upgradeSettings().legacyGitSelfUpdate) {
|
|
28
|
+
return c.json({ success: false, error: { code: 'SELF_UPDATE_RETIRED',
|
|
29
|
+
message: 'In-place git self-update is off. Upgrade with `magnus upgrade` (or `magnus-server upgrade`) on the server host; '
|
|
30
|
+
+ 'set MAGNUS_LEGACY_GIT_SELF_UPDATE=true to re-enable this endpoint for a source checkout.',
|
|
31
|
+
upgradeCommand: 'magnus upgrade' } }, 410);
|
|
32
|
+
}
|
|
33
|
+
await next();
|
|
34
|
+
};
|
|
35
|
+
selfUpdateRouter.use('/check', legacyGitSelfUpdate);
|
|
36
|
+
selfUpdateRouter.use('/apply', legacyGitSelfUpdate);
|
|
37
|
+
selfUpdateRouter.use('/restart', legacyGitSelfUpdate);
|
|
19
38
|
selfUpdateRouter.get('/work/:workId', requireRole('ADMIN', 'OPERATOR'), async (c) => {
|
|
20
39
|
const id = z.string().guid().safeParse(c.req.param('workId'));
|
|
21
40
|
if (!id.success)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { Hono } from 'hono';
|
|
2
|
+
import { authMiddleware, requireRole } from '../middleware/auth.js';
|
|
3
|
+
import { upgradeSettings } from '../config/features.js';
|
|
4
|
+
import { CONTROL_API_VERSION } from '../config/version.js';
|
|
5
|
+
import { agentFixCommand, agentVersionSkew, getUpdateCheck } from '../services/update-check-service.js';
|
|
6
|
+
/**
|
|
7
|
+
* GET /api/v1/system/updates: available releases (MAGNUS_UPDATE_CHECK) and
|
|
8
|
+
* agents running a different version than the server ships with.
|
|
9
|
+
*/
|
|
10
|
+
export const systemUpdatesRouter = new Hono();
|
|
11
|
+
systemUpdatesRouter.use('*', authMiddleware);
|
|
12
|
+
async function report(refresh) {
|
|
13
|
+
const [server, agents] = await Promise.all([getUpdateCheck({ refresh }), agentVersionSkew()]);
|
|
14
|
+
const settings = upgradeSettings();
|
|
15
|
+
return {
|
|
16
|
+
version: CONTROL_API_VERSION,
|
|
17
|
+
server,
|
|
18
|
+
agents: {
|
|
19
|
+
...agents,
|
|
20
|
+
upgradesEnabled: settings.agentUpgrades,
|
|
21
|
+
fixCommand: agents.behind.length > 0 ? agentFixCommand(agents.behind.map(node => node.name)) : null,
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
systemUpdatesRouter.get('/', async (c) => c.json({ success: true, data: await report(false) }));
|
|
26
|
+
systemUpdatesRouter.post('/check', requireRole('ADMIN', 'OPERATOR'), async (c) => c.json({ success: true, data: await report(true) }));
|
|
27
|
+
//# sourceMappingURL=system-updates.js.map
|