197 lines
7.8 KiB
TypeScript
197 lines
7.8 KiB
TypeScript
/**
|
|
* systemd-unit-install smoke (beta14).
|
|
*
|
|
* Guards `ops/scripts/install-systemd-units.sh` — the installer that
|
|
* re-points the core systemd units at whatever path the repo was
|
|
* actually cloned to (the `~/morphit`-vs-`/opt/morphit` fix that
|
|
* retires the old `systemctl edit` drop-in note).
|
|
*
|
|
* What it checks:
|
|
*
|
|
* 1. The installer exists and is a real script (shebang).
|
|
* 2. It self-locates the repo root from its own location
|
|
* (so it works wherever you cloned), substitutes the hardcoded
|
|
* `/opt/morphit` base, and runs `systemctl daemon-reload`.
|
|
* 3. It targets exactly the three monorepo services
|
|
* (indexer / relay / matrix-bot) — and NOT morphit-mcp, whose
|
|
* separate restricted directory is a deliberate least-privilege
|
|
* isolation.
|
|
* 4. Applying the installer's substitution to each core unit with
|
|
* a sentinel checkout path leaves NO `/opt/morphit` behind,
|
|
* lands the sentinel path in WorkingDirectory, and preserves
|
|
* the [Unit]/[Service]/[Install] structure.
|
|
* 5. The mcp unit still references its own isolated
|
|
* directory (isolation not flattened).
|
|
* 6. Inline-comment guard: no unit file carries an inline `#`
|
|
* comment on a directive line — systemd treats it as part of
|
|
* the value and silently misparses it (the matrix-bot
|
|
* `MemoryDenyWriteExecute` defect fixed in the same pass).
|
|
*
|
|
* Runtime: a few ms (pure file reads + string ops). No root, no
|
|
* systemd needed — it tests the substitution logic, not the live
|
|
* `systemctl` behaviour (which is the operator's real-box step).
|
|
*
|
|
* To tamper-test: re-introduce an inline comment in any unit, or
|
|
* point the installer at a different DEFAULT_DIR, and re-run.
|
|
*/
|
|
|
|
import { readFileSync, existsSync } from 'node:fs';
|
|
import { dirname, resolve, join } from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
const ANSI_GREEN = '\x1b[32m';
|
|
const ANSI_RED = '\x1b[31m';
|
|
const ANSI_RESET = '\x1b[0m';
|
|
|
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
const REPO = resolve(HERE, '..');
|
|
const INSTALLER = join(REPO, 'ops/scripts/install-systemd-units.sh');
|
|
const UNIT_DIR = join(REPO, 'ops/systemd');
|
|
|
|
const DEFAULT_DIR = '/opt/morphit';
|
|
const SENTINEL = '/home/tester/morphit-checkout';
|
|
const CORE_UNITS = [
|
|
'morphit-indexer.service',
|
|
'morphit-relay.service',
|
|
'morphit-matrix-bot.service'
|
|
];
|
|
// morphit-mcp deliberately runs from its own restricted directory
|
|
// (/opt/morphit-mcp) as a low-priv user — the installer must NOT fold
|
|
// it into the monorepo core loop. (The morphit-relay-mint-acts oneshot
|
|
// was the other isolated unit; its ACT-minting model was removed at
|
|
// beta.28 / cp329 — account creation is now a direct account_create op
|
|
// paying the fee inline — and the unit templates were deleted at cp348.)
|
|
const ISOLATED_UNITS = ['morphit-mcp.service'];
|
|
|
|
interface Result {
|
|
name: string;
|
|
passed: boolean;
|
|
detail?: string;
|
|
}
|
|
const results: Result[] = [];
|
|
const check = (name: string, passed: boolean, detail?: string) =>
|
|
results.push({ name, passed, detail });
|
|
|
|
// ── 1. Installer exists + is a script ──────────────────────────────
|
|
const installerExists = existsSync(INSTALLER);
|
|
check('installer script exists at ops/scripts/install-systemd-units.sh', installerExists);
|
|
|
|
const installer = installerExists ? readFileSync(INSTALLER, 'utf8') : '';
|
|
check(
|
|
'installer has a shebang',
|
|
installer.startsWith('#!') && /bash/.test(installer.split('\n')[0])
|
|
);
|
|
|
|
// ── 2. Self-locating + substitution + daemon-reload ────────────────
|
|
check(
|
|
'installer self-locates repo root from its own path',
|
|
/BASH_SOURCE\[0\]/.test(installer) && /\.\.\/\.\./.test(installer)
|
|
);
|
|
check(
|
|
`installer rewrites the hardcoded ${DEFAULT_DIR} base`,
|
|
installer.includes(`DEFAULT_DIR="${DEFAULT_DIR}"`) &&
|
|
/sed "s#\$\{DEFAULT_DIR\}#\$\{REPO_DIR\}#g"/.test(installer)
|
|
);
|
|
check('installer runs systemctl daemon-reload', /systemctl daemon-reload/.test(installer));
|
|
|
|
// ── 3. Targets the 3 monorepo units, not the isolated ones ─────────
|
|
for (const u of CORE_UNITS) {
|
|
check(`installer targets ${u}`, installer.includes(u));
|
|
}
|
|
// Scope the isolation invariant to EXECUTABLE logic. The installer is
|
|
// allowed to *document* how to set up the isolated units by hand (it
|
|
// echoes step-by-step guidance for MCP + web push), but its actual
|
|
// install loop (CORE_UNITS) must never include them. Strip comment +
|
|
// standalone-echo lines before checking, so guidance text doesn't trip
|
|
// the invariant.
|
|
const installerLogic = installer
|
|
.split('\n')
|
|
.filter((ln) => !/^\s*#/.test(ln) && !/^\s*echo\b/.test(ln))
|
|
.join('\n');
|
|
for (const u of ISOLATED_UNITS) {
|
|
check(
|
|
`installer does NOT install ${u} in its core loop (isolation preserved)`,
|
|
!installerLogic.includes(u)
|
|
);
|
|
}
|
|
|
|
// ── 4. Substitution correctness on each core unit ──────────────────
|
|
const substitute = (text: string) => text.split(DEFAULT_DIR).join(SENTINEL);
|
|
for (const u of CORE_UNITS) {
|
|
const p = join(UNIT_DIR, u);
|
|
if (!existsSync(p)) {
|
|
check(`${u} present in ops/systemd/`, false, `missing: ${p}`);
|
|
continue;
|
|
}
|
|
const out = substitute(readFileSync(p, 'utf8'));
|
|
check(`${u}: no ${DEFAULT_DIR} survives substitution`, !out.includes(DEFAULT_DIR));
|
|
const wd = out.split('\n').find((l) => l.startsWith('WorkingDirectory='));
|
|
check(
|
|
`${u}: WorkingDirectory points into the checkout`,
|
|
wd !== undefined && wd.includes(SENTINEL)
|
|
);
|
|
check(
|
|
`${u}: [Unit]/[Service]/[Install] sections preserved`,
|
|
out.includes('[Unit]') && out.includes('[Service]') && out.includes('[Install]')
|
|
);
|
|
}
|
|
|
|
// ── 5. Isolated units keep their own restricted dirs ───────────────
|
|
{
|
|
const mcp = join(UNIT_DIR, 'morphit-mcp.service');
|
|
check(
|
|
'morphit-mcp keeps its own /opt/morphit-mcp dir',
|
|
existsSync(mcp) && readFileSync(mcp, 'utf8').includes('/opt/morphit-mcp')
|
|
);
|
|
}
|
|
|
|
// ── 6. Inline-comment guard across ALL units ───────────────────────
|
|
// systemd only treats a line as a comment when it STARTS with '#'
|
|
// (or ';'); an inline '# ...' after a directive value is parsed as
|
|
// part of the value and misparsed. Flag any directive line carrying
|
|
// a '#'.
|
|
{
|
|
const fs = readFileSync; // alias to keep the loop terse
|
|
const allUnits = [...CORE_UNITS, ...ISOLATED_UNITS];
|
|
const offenders: string[] = [];
|
|
for (const u of allUnits) {
|
|
const p = join(UNIT_DIR, u);
|
|
if (!existsSync(p)) continue;
|
|
const lines = fs(p, 'utf8').split('\n');
|
|
lines.forEach((line, i) => {
|
|
// A directive line looks like Key=Value. Comments/sections
|
|
// start with # or [. Documentation= URLs legitimately have
|
|
// no '#'. If a Key=Value line contains '#', it's an inline
|
|
// comment (or a stray '#') systemd would misparse.
|
|
if (/^[A-Za-z][A-Za-z0-9]*=/.test(line) && line.includes('#')) {
|
|
offenders.push(`${u}:${i + 1} ${line.trim()}`);
|
|
}
|
|
});
|
|
}
|
|
check(
|
|
'no unit carries an inline # comment on a directive line',
|
|
offenders.length === 0,
|
|
offenders.join('\n ')
|
|
);
|
|
}
|
|
|
|
// ── report ─────────────────────────────────────────────────────────
|
|
let failed = 0;
|
|
for (const r of results) {
|
|
if (r.passed) {
|
|
console.log(` ${ANSI_GREEN}✓${ANSI_RESET} ${r.name}`);
|
|
} else {
|
|
console.log(` ${ANSI_RED}✗${ANSI_RESET} ${r.name}`);
|
|
if (r.detail) console.log(` ${r.detail}`);
|
|
failed++;
|
|
}
|
|
}
|
|
|
|
console.log();
|
|
console.log('──────────────────────────────────────────────────────');
|
|
if (failed > 0) {
|
|
console.log(`✗ ${failed} of ${results.length} scenarios failed`);
|
|
process.exit(1);
|
|
} else {
|
|
console.log(`✓ all ${results.length} scenarios passed`);
|
|
}
|