openvibers/openvibe.host Commits

ovhost: release-layout services (root-owned releases/<id> behind current, installed by their own deploy script) are validated by their current release instead of as a git checkout and are never managed; OpenRe uses it

OP OpenVibers committed 23 Sept 2026 · 93f2de7cae · 5 files, +47 −6


Changes

ddiff --git a/host.example.json b/host.example.json
iindex cad90c9..8efe602 100644
---- a/host.example.json
++++ b/host.example.json
@@@ -552,6 +552,7 @@
"openre": {
"_note": "Release layout: the service runs from /opt/openre.stream/current and deploys with its own deploy/deploy.sh (release | api | workers). The templated ingest and restream workers belong to the session coordinator; ovhost never restarts them.",
"repo": "/opt/openre.stream",
++ "layout": "release",
"units": ["openre-api.service", "openre-session-coordinator.service"],
"envFile": "/etc/openvibe/openre.env",
"env": { "required": "from-example" },
ddiff --git a/lib/commands/status.js b/lib/commands/status.js
iindex 0d9558f..9cbc387 100644
---- a/lib/commands/status.js
++++ b/lib/commands/status.js
@@@ -17,9 +17,15 @@ async function statusOne(ctx, svc) {
row.socket = { unit: svc.socketUnit, active: st.active, sub: st.sub };
}
try {
-- const g = git(exec, svc);
-- row.sha = await g.head();
-- row.branch = await g.branch();
++ if (svc.layout === 'release') {
++ const target = await exec.readlink(`${svc.repo}/current`);
++ row.sha = target ? String(target).split('/').pop() : null;
++ row.branch = 'release';
++ } else {
++ const g = git(exec, svc);
++ row.sha = await g.head();
++ row.branch = await g.branch();
++ }
} catch (err) {
row.shaError = err.message.split('\n')[0];
}
ddiff --git a/lib/commands/validate.js b/lib/commands/validate.js
iindex aded1e7..01b9026 100644
---- a/lib/commands/validate.js
++++ b/lib/commands/validate.js
@@@ -31,7 +31,16 @@ async function validate(ctx, id) {
// ── checkout ──
const repoStat = await exec.stat(svc.repo);
if (!repoStat || !repoStat.isDir) add('error', 'checkout', `${svc.repo} does not exist`);
-- else {
++ else if (svc.layout === 'release') {
++ // Release layout: <repo>/current -> <repo>/releases/<id>, root-owned so the service user
++ // cannot rewrite the code it runs.
++ const cur = `${svc.repo}/current`;
++ const target = await exec.readlink(cur);
++ const st = target ? await exec.stat(cur) : null;
++ if (!target || !st || !st.isDir) add('error', 'checkout', `${cur} is not a symlink to a release directory`);
++ else if (repoStat.owner !== 'root' || st.owner !== 'root') add('error', 'checkout', `${svc.repo} and its current release must be root-owned (found ${repoStat.owner}/${st.owner})`);
++ else add('ok', 'checkout', `release layout: current -> ${target}`);
++ } else {
if (repoStat.owner !== svc.owner) add('error', 'checkout', `${svc.repo} is owned by ${repoStat.owner}, expected ${svc.owner}`);
else add('ok', 'checkout', `${svc.repo} owned by ${svc.owner}`);
try {
ddiff --git a/lib/inventory.js b/lib/inventory.js
iindex 97f3417..3e912ce 100644
---- a/lib/inventory.js
++++ b/lib/inventory.js
@@@ -43,8 +43,11 @@ function normaliseService(id, raw, defaults) {
runAs: s.runAs || s.owner,
remote: s.remote || 'origin',
branch: s.branch || 'main',
-- managed: s.managed !== false,
-- unmanagedReason: s.unmanagedReason || null,
++ // 'git' (a checkout ovhost can move) or 'release' (root-owned releases/<id> behind a
++ // `current` symlink, installed by the service's own deploy script: never managed here).
++ layout: s.layout === 'release' ? 'release' : 'git',
++ managed: s.layout === 'release' ? false : s.managed !== false,
++ unmanagedReason: s.unmanagedReason || (s.layout === 'release' ? 'release layout: deployed by its own deploy script' : null),
units: s.units || [],
socketUnit: s.socketUnit || null,
unitSources: s.unitSources || {},
ddiff --git a/test/validate.test.js b/test/validate.test.js
iindex 3cfdf1d..98ffcec 100644
---- a/test/validate.test.js
++++ b/test/validate.test.js
@@@ -9,6 +9,28 @@ function noSecrets(text) {
}
runTests([
++ test('a release-layout service is checked by its current release, not as a git checkout, and is never managed', async () => {
++ const host = scenario();
++ const raw = JSON.parse(host.read('/etc/openvibe/host.json'));
++ raw.services.rel = { repo: '/opt/rel.stream', layout: 'release', units: [] };
++ host.put('/etc/openvibe/host.json', JSON.stringify(raw), { mode: 0o640 });
++ host.put('/opt/rel.stream/releases/655b98a10aaa/package.json', '{}');
++ await host.exec.symlink('/opt/rel.stream/releases/655b98a10aaa', '/opt/rel.stream/current');
++ let res = JSON.parse((await host.cli('validate', 'rel', '--json')).out);
++ assert.deepStrictEqual(res.findings.filter((f) => f.level === 'error'), []);
++ assert.ok(res.findings.some((f) => f.area === 'checkout' && /current -> \/opt\/rel\.stream\/releases\/655b98a10aaa/.test(f.message)));
++ const st = await host.cli('status', '--json');
++ assert.match(st.out, /655b98a10aaa/);
++ assert.match(st.out, /"managed":\s*false/);
++ // A release the service user could rewrite is refused.
++ host.put('/opt/rel.stream/releases/bad/package.json', '{}', { owner: 'ubuntu' });
++ host.files.get('/opt/rel.stream/releases/bad').owner = 'ubuntu';
++ host.files.delete('/opt/rel.stream/current');
++ await host.exec.symlink('/opt/rel.stream/releases/bad', '/opt/rel.stream/current');
++ res = JSON.parse((await host.cli('validate', 'rel', '--json')).out);
++ assert.ok(res.findings.some((f) => f.level === 'error' && /must be root-owned/.test(f.message)));
++ }),
++
test('env validation reports names only — never a value — in text and JSON output', async () => {
const host = scenario();
host.put('/etc/openvibe/live.env', `JWT_SECRET=${SECRET}\nBASE_URL=\nPAYPAL_CLIENT_SECRET=""\n`, { mode: 0o644 });