@hivemagnus/magnus-agent 1.1.0 → 1.1.2
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/dist/executors/cron-manager.js +2 -1
- package/dist/executors/hermes-manager.d.ts +1 -1
- package/dist/hermes-sync.js +10 -2
- package/dist/upgrade/install-service.js +7 -1
- package/dist/upgrade/self-upgrade.d.ts +6 -0
- package/dist/upgrade/self-upgrade.js +28 -1
- package/dist/upgrade/stable-path.d.ts +2 -0
- package/dist/upgrade/stable-path.js +33 -0
- package/launcher/launcher.mjs +10 -1
- package/package.json +8 -8
|
@@ -11,6 +11,7 @@ import { hash } from './execution-supervisor-state.js';
|
|
|
11
11
|
import { HOST_FQDN, NODE_MANAGER_SECRET, RESULT_SIGNING_SECRET } from '../env.js';
|
|
12
12
|
import { readdir, readFile } from 'node:fs/promises';
|
|
13
13
|
import { createHash, createHmac, timingSafeEqual } from 'node:crypto';
|
|
14
|
+
import { stableCodePath } from '../upgrade/stable-path.js';
|
|
14
15
|
export const cronRoutes = new Hono();
|
|
15
16
|
const scheduler = new ManagedScheduler();
|
|
16
17
|
const execFileAsync = promisify(execFile);
|
|
@@ -53,7 +54,7 @@ export function removeCronEntry(raw, name) {
|
|
|
53
54
|
}
|
|
54
55
|
return result.join('\n');
|
|
55
56
|
}
|
|
56
|
-
export function managedCronLine(job, nodePath = process.execPath, runnerPath = fileURLToPath(new URL('./scheduler-runner.js', import.meta.url))) {
|
|
57
|
+
export function managedCronLine(job, nodePath = process.execPath, runnerPath = stableCodePath(fileURLToPath(new URL('./scheduler-runner.js', import.meta.url)))) {
|
|
57
58
|
for (const path of [nodePath, runnerPath])
|
|
58
59
|
if (!path.startsWith('/') || /[\x00-\x1f\x7f%]/.test(path))
|
|
59
60
|
throw new Error('Unsafe fixed cron runner path');
|
|
@@ -85,8 +85,8 @@ declare const configurationSchema: z.ZodObject<{
|
|
|
85
85
|
token: z.ZodOptional<z.ZodString>;
|
|
86
86
|
workspaceId: z.ZodDefault<z.ZodString>;
|
|
87
87
|
defaultScope: z.ZodDefault<z.ZodEnum<{
|
|
88
|
-
private: "private";
|
|
89
88
|
shared: "shared";
|
|
89
|
+
private: "private";
|
|
90
90
|
}>>;
|
|
91
91
|
recallMode: z.ZodDefault<z.ZodEnum<{
|
|
92
92
|
context: "context";
|
package/dist/hermes-sync.js
CHANGED
|
@@ -2,14 +2,22 @@
|
|
|
2
2
|
import { config } from 'dotenv';
|
|
3
3
|
import { createHash, randomUUID } from 'node:crypto';
|
|
4
4
|
import { spawn } from 'node:child_process';
|
|
5
|
-
import { constants, createReadStream } from 'node:fs';
|
|
5
|
+
import { constants, createReadStream, realpathSync } from 'node:fs';
|
|
6
6
|
import { access, chown, chmod, lstat, mkdir, mkdtemp, readFile, readlink, readdir, rename, rm, stat, symlink, writeFile } from 'node:fs/promises';
|
|
7
7
|
import { dirname, isAbsolute, join, resolve } from 'node:path';
|
|
8
8
|
import { tmpdir } from 'node:os';
|
|
9
9
|
import { fileURLToPath } from 'node:url';
|
|
10
10
|
import { childEnvironment } from './utils/child-env.js';
|
|
11
11
|
import { armChildTimeout } from './utils/child-timeout.js';
|
|
12
|
-
|
|
12
|
+
// Compare real paths: the service may start this file through a link (node_modules -> current/node_modules).
|
|
13
|
+
const invokedAsService = (() => {
|
|
14
|
+
try {
|
|
15
|
+
return Boolean(process.argv[1] && realpathSync(resolve(process.argv[1])) === realpathSync(fileURLToPath(import.meta.url)));
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
})();
|
|
13
21
|
if (invokedAsService)
|
|
14
22
|
config({ path: join(process.env['AGENT_INSTALL_DIR'] ?? '/opt/magnus-agent', '.env'), quiet: true });
|
|
15
23
|
const api = new URL('/api/v1/hermes-registry/', process.env['CONTROL_API_URL'] ?? 'https://localhost:4000');
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* root for it and never calls sudo.
|
|
13
13
|
*/
|
|
14
14
|
import { execFileSync } from 'node:child_process';
|
|
15
|
-
import { copyFileSync, existsSync, lstatSync, mkdirSync, readFileSync, realpathSync, renameSync, writeFileSync } from 'node:fs';
|
|
15
|
+
import { copyFileSync, existsSync, lstatSync, mkdirSync, readFileSync, realpathSync, renameSync, symlinkSync, writeFileSync } from 'node:fs';
|
|
16
16
|
import { dirname, join, resolve } from 'node:path';
|
|
17
17
|
import { fileURLToPath } from 'node:url';
|
|
18
18
|
import { AGENT_PACKAGE, pointCurrentAt } from './self-upgrade.js';
|
|
@@ -188,6 +188,10 @@ export async function installService(argv) {
|
|
|
188
188
|
}
|
|
189
189
|
act(`point ${current} at versions/${version}`, () => pointCurrentAt(dir, version));
|
|
190
190
|
}
|
|
191
|
+
// Paths written before the versioned layout (cron lines, other units) keep working.
|
|
192
|
+
if (!existsSync(join(dir, 'node_modules')) && !isSymlink(join(dir, 'node_modules'))) {
|
|
193
|
+
act(`link ${join(dir, 'node_modules')} to current/node_modules`, () => symlinkSync(join('current', 'node_modules'), join(dir, 'node_modules'), 'dir'));
|
|
194
|
+
}
|
|
191
195
|
// The launcher is copied from the release that runs install-service, not linked:
|
|
192
196
|
// self-upgrades never replace it.
|
|
193
197
|
act(`install ${join(dir, 'launcher', 'launcher.mjs')}`, () => {
|
|
@@ -205,6 +209,8 @@ export async function installService(argv) {
|
|
|
205
209
|
execFileSync('chown', [`${user}:${user}`, dir]);
|
|
206
210
|
execFileSync('chown', ['-R', `${user}:${user}`, join(dir, 'versions'), join(dir, '.npm')]);
|
|
207
211
|
execFileSync('chown', ['-h', `${user}:${user}`, current]);
|
|
212
|
+
if (isSymlink(join(dir, 'node_modules')))
|
|
213
|
+
execFileSync('chown', ['-h', `${user}:${user}`, join(dir, 'node_modules')]);
|
|
208
214
|
});
|
|
209
215
|
}
|
|
210
216
|
act(`write ${options.unitPath}${existsSync(options.unitPath) ? ` (previous unit kept as ${options.unitPath}.pre-launcher)` : ''}`, () => {
|
|
@@ -14,6 +14,12 @@ export interface UpgradeState {
|
|
|
14
14
|
}
|
|
15
15
|
export declare function installDir(env?: NodeJS.ProcessEnv): string;
|
|
16
16
|
export declare function readUpgradeState(dir: string): UpgradeState | null;
|
|
17
|
+
/**
|
|
18
|
+
* An agent running as root inside a directory another account owns (a deploy
|
|
19
|
+
* user that installs releases there) gives what it writes to that owner, so
|
|
20
|
+
* the owner can still replace or remove it. Otherwise this does nothing.
|
|
21
|
+
*/
|
|
22
|
+
export declare function adoptInstallOwner(dir: string, path: string, uid?: number | undefined): void;
|
|
17
23
|
export declare function writeUpgradeState(dir: string, state: UpgradeState): void;
|
|
18
24
|
export declare function pointCurrentAt(dir: string, version: string): void;
|
|
19
25
|
export type Layout = AgentSelfUpgradeStatus['layout'];
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import { createHash } from 'node:crypto';
|
|
11
11
|
import { spawn } from 'node:child_process';
|
|
12
|
-
import { accessSync, constants, existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, symlinkSync, writeFileSync } from 'node:fs';
|
|
12
|
+
import { accessSync, constants, existsSync, lchownSync, lstatSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, statSync, symlinkSync, writeFileSync } from 'node:fs';
|
|
13
13
|
import { join, resolve } from 'node:path';
|
|
14
14
|
import { AGENT_SELF_UPGRADE_PROTOCOL, checkUpgradeAllowed, compareVersions, } from '@hivemagnus/magnus-contracts';
|
|
15
15
|
export const STATE_FILE = 'upgrade-state.json';
|
|
@@ -25,16 +25,39 @@ export function readUpgradeState(dir) {
|
|
|
25
25
|
return null;
|
|
26
26
|
}
|
|
27
27
|
}
|
|
28
|
+
/**
|
|
29
|
+
* An agent running as root inside a directory another account owns (a deploy
|
|
30
|
+
* user that installs releases there) gives what it writes to that owner, so
|
|
31
|
+
* the owner can still replace or remove it. Otherwise this does nothing.
|
|
32
|
+
*/
|
|
33
|
+
export function adoptInstallOwner(dir, path, uid = process.getuid?.()) {
|
|
34
|
+
if (uid !== 0)
|
|
35
|
+
return;
|
|
36
|
+
const owner = statSync(dir);
|
|
37
|
+
if (owner.uid === 0)
|
|
38
|
+
return;
|
|
39
|
+
const walk = (entry) => {
|
|
40
|
+
const info = lstatSync(entry);
|
|
41
|
+
lchownSync(entry, owner.uid, owner.gid);
|
|
42
|
+
if (info.isDirectory())
|
|
43
|
+
for (const child of readdirSync(entry))
|
|
44
|
+
walk(join(entry, child));
|
|
45
|
+
};
|
|
46
|
+
if (lstatSync(path, { throwIfNoEntry: false }))
|
|
47
|
+
walk(path);
|
|
48
|
+
}
|
|
28
49
|
export function writeUpgradeState(dir, state) {
|
|
29
50
|
const path = join(dir, STATE_FILE);
|
|
30
51
|
const temporary = `${path}.${process.pid}.tmp`;
|
|
31
52
|
writeFileSync(temporary, `${JSON.stringify(state, null, 2)}\n`, { mode: 0o600 });
|
|
53
|
+
adoptInstallOwner(dir, temporary);
|
|
32
54
|
renameSync(temporary, path);
|
|
33
55
|
}
|
|
34
56
|
export function pointCurrentAt(dir, version) {
|
|
35
57
|
const temporary = join(dir, `current.${process.pid}.tmp`);
|
|
36
58
|
rmSync(temporary, { force: true });
|
|
37
59
|
symlinkSync(join('versions', version), temporary, 'dir');
|
|
60
|
+
adoptInstallOwner(dir, temporary);
|
|
38
61
|
renameSync(temporary, join(dir, 'current'));
|
|
39
62
|
}
|
|
40
63
|
export function detectLayout(dir, env = process.env) {
|
|
@@ -228,6 +251,8 @@ export async function stageUpgrade(options) {
|
|
|
228
251
|
}
|
|
229
252
|
rmSync(target, { recursive: true, force: true });
|
|
230
253
|
mkdirSync(join(dir, 'versions'), { recursive: true });
|
|
254
|
+
adoptInstallOwner(dir, join(dir, 'versions'));
|
|
255
|
+
adoptInstallOwner(dir, install);
|
|
231
256
|
renameSync(install, target);
|
|
232
257
|
const now = (options.now ?? new Date()).toISOString();
|
|
233
258
|
writeUpgradeState(dir, { state: 'staged', from, to: request.version, requestId: request.requestId, attempt: 0, maxAttempts: 2, reason: null, updatedAt: now });
|
|
@@ -236,6 +261,8 @@ export async function stageUpgrade(options) {
|
|
|
236
261
|
}
|
|
237
262
|
finally {
|
|
238
263
|
rmSync(work, { recursive: true, force: true });
|
|
264
|
+
adoptInstallOwner(dir, join(dir, 'staging'));
|
|
265
|
+
adoptInstallOwner(dir, join(dir, '.npm'));
|
|
239
266
|
staging = false;
|
|
240
267
|
}
|
|
241
268
|
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Paths to this agent's own code that outlive the process (cron lines, for
|
|
3
|
+
* example) must not name versions/<X>: a self-upgrade moves ./current and later
|
|
4
|
+
* prunes old versions. Under the launcher, such a path is rewritten through the
|
|
5
|
+
* install directory: `<dir>/node_modules/…` when that is the compatibility link
|
|
6
|
+
* to current/node_modules (so lines written before the versioned layout keep
|
|
7
|
+
* their exact text), otherwise `<dir>/current/…`.
|
|
8
|
+
*/
|
|
9
|
+
import { lstatSync, realpathSync } from 'node:fs';
|
|
10
|
+
import { join, relative, resolve, sep } from 'node:path';
|
|
11
|
+
export function stableCodePath(path, env = process.env) {
|
|
12
|
+
if (env['MAGNUS_AGENT_LAUNCHER'] !== '1' || !env['AGENT_INSTALL_DIR'])
|
|
13
|
+
return path;
|
|
14
|
+
const dir = resolve(env['AGENT_INSTALL_DIR']);
|
|
15
|
+
const inside = relative(join(dir, 'versions'), path);
|
|
16
|
+
if (inside.startsWith('..') || inside === '' || inside.startsWith(sep))
|
|
17
|
+
return path;
|
|
18
|
+
const [, ...rest] = inside.split(sep);
|
|
19
|
+
if (rest.length === 0)
|
|
20
|
+
return path;
|
|
21
|
+
try {
|
|
22
|
+
const link = join(dir, 'node_modules');
|
|
23
|
+
if (rest[0] === 'node_modules' && lstatSync(link).isSymbolicLink()
|
|
24
|
+
&& realpathSync(link) === realpathSync(join(dir, 'current', 'node_modules'))) {
|
|
25
|
+
return join(dir, ...rest);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
// no compatibility link
|
|
30
|
+
}
|
|
31
|
+
return join(dir, 'current', ...rest);
|
|
32
|
+
}
|
|
33
|
+
//# sourceMappingURL=stable-path.js.map
|
package/launcher/launcher.mjs
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* `magnus-agent install-service` replaces it. It never escalates privileges.
|
|
15
15
|
*/
|
|
16
16
|
import { spawn } from 'node:child_process';
|
|
17
|
-
import { existsSync,
|
|
17
|
+
import { closeSync, existsSync, fsyncSync, lchownSync, openSync, readFileSync, renameSync, rmSync, statSync, symlinkSync, writeFileSync } from 'node:fs';
|
|
18
18
|
import { dirname, join, resolve } from 'node:path';
|
|
19
19
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
20
20
|
|
|
@@ -61,12 +61,20 @@ export function readState(dir) {
|
|
|
61
61
|
}
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
+
/** Running as root in a directory another account owns: hand what we write to that owner. */
|
|
65
|
+
function adoptOwner(dir, path) {
|
|
66
|
+
if (process.getuid?.() !== 0) return;
|
|
67
|
+
const owner = statSync(dir);
|
|
68
|
+
if (owner.uid !== 0) lchownSync(path, owner.uid, owner.gid);
|
|
69
|
+
}
|
|
70
|
+
|
|
64
71
|
export function writeState(dir, state) {
|
|
65
72
|
const path = join(dir, STATE_FILE);
|
|
66
73
|
const temporary = `${path}.${process.pid}.tmp`;
|
|
67
74
|
writeFileSync(temporary, `${JSON.stringify(state, null, 2)}\n`, { mode: 0o600 });
|
|
68
75
|
const fd = openSync(temporary, 'r');
|
|
69
76
|
try { fsyncSync(fd); } finally { closeSync(fd); }
|
|
77
|
+
adoptOwner(dir, temporary);
|
|
70
78
|
renameSync(temporary, path);
|
|
71
79
|
}
|
|
72
80
|
|
|
@@ -74,6 +82,7 @@ export function pointCurrentAt(dir, version) {
|
|
|
74
82
|
const temporary = join(dir, `current.${process.pid}.tmp`);
|
|
75
83
|
rmSync(temporary, { force: true });
|
|
76
84
|
symlinkSync(join('versions', version), temporary, 'dir');
|
|
85
|
+
adoptOwner(dir, temporary);
|
|
77
86
|
renameSync(temporary, join(dir, 'current'));
|
|
78
87
|
}
|
|
79
88
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hivemagnus/magnus-agent",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.2",
|
|
4
4
|
"description": "Magnus node agent: a small daemon that enrolls a Linux host with a magnus-server and runs authenticated health, command, cron, file and skill management, with optional Hermes Agent, Codex and Ardent management.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"magnus",
|
|
@@ -55,22 +55,22 @@
|
|
|
55
55
|
"clean": "node -e \"const fs=require('node:fs');fs.rmSync('dist',{recursive:true,force:true});fs.rmSync('tsconfig.tsbuildinfo',{force:true})\""
|
|
56
56
|
},
|
|
57
57
|
"dependencies": {
|
|
58
|
-
"@hivemagnus/magnus-contracts": "1.1.
|
|
58
|
+
"@hivemagnus/magnus-contracts": "1.1.2",
|
|
59
59
|
"@hono/node-server": "^2.1.5",
|
|
60
60
|
"dotenv": "^18.0.9",
|
|
61
|
-
"hono": "^4.13.
|
|
61
|
+
"hono": "^4.13.16",
|
|
62
62
|
"ky": "^2.2.0",
|
|
63
|
-
"nanoid": "^
|
|
63
|
+
"nanoid": "^6.0.2",
|
|
64
64
|
"tar-stream": "3.2.2",
|
|
65
|
-
"undici": "^
|
|
65
|
+
"undici": "^8.12.3",
|
|
66
66
|
"yaml": "^2.9.1",
|
|
67
67
|
"zod": "^4.6.7"
|
|
68
68
|
},
|
|
69
69
|
"devDependencies": {
|
|
70
|
-
"@types/node": "^
|
|
70
|
+
"@types/node": "^24.19.2",
|
|
71
71
|
"@types/tar-stream": "^3.1.5",
|
|
72
72
|
"tsx": "^4.23.15",
|
|
73
|
-
"typescript": "^
|
|
74
|
-
"vitest": "^
|
|
73
|
+
"typescript": "^6.0.3",
|
|
74
|
+
"vitest": "^5.0.5"
|
|
75
75
|
}
|
|
76
76
|
}
|