Add read-only hostd ops for update auditing (Sam & Claude)

Closes the third leg of today's update-question arc. Sysadmin (and any
tenant agent diagnosing its own jail) can now answer "any updates
available?" without root by calling these hostd ops directly:

- pkg-audit (with optional jail param) — runs `pkg audit -F` to fetch
  the vulnerability database and report CVEs against installed packages.
  Jail-aware via bastille cmd, mirroring the service-status pattern.
- freebsd-update-status — runs `freebsd-update updatesready`. Exits 0
  (none ready) or 2 (ready); no fetch, no install. The actual fetch +
  install lives in the daily 06:50 cron from system-update-cron.
- freebsd-version — runs `freebsd-version -kru` for kernel/running/
  userland visibility. Useful for the reboot-pending check that
  system-update already does internally.

Authorization: all three are unconditionally allowed in
hostd-authorization.ts alongside ping / bastille-list / pkg-version —
they're diagnostic reads, never mutate state, safe for tenant-agents.

Per docs/internal/SUDO_REPLACEMENT.md, this is the right shape: extend
hostd's read-only surface rather than reach for mac_do/sudo replacement.

---
2259 tests passing locally (+5 covering host-default dispatch, jail
dispatch, freebsd-update-status, freebsd-version, plus tenant-readable
auth). Pre-existing argon2/controlplane-*/cms.test failures unchanged.

---
Build: FAIL | Tests: FAIL — 16 failed

---
Build: FAIL | Tests: FAIL — 16 failed
This commit is contained in:
Operator & Claude Code 2026-05-10 09:26:17 +02:00
parent 26336acaaf
commit 16bea08721
4 changed files with 77 additions and 0 deletions

View file

@ -75,6 +75,24 @@ describe('authorizeHostdOperation', () => {
).toEqual({ allowed: true, owner: 'shared-platform' });
});
it('allows read-only update audits for tenant agents', () => {
// pkg-audit, freebsd-update-status, freebsd-version are diagnostic reads —
// tenant agents should be able to call them without operator approval.
for (const op of [
'pkg-audit',
'freebsd-update-status',
'freebsd-version',
] as const) {
expect(
authorizeHostdOperation(
op,
{},
{ tenantId: 'alpha', caller: 'tenant-agent', registry },
),
).toEqual({ allowed: true });
}
});
it('blocks shared jails for tenant agents', () => {
expect(
authorizeHostdOperation(

View file

@ -81,6 +81,9 @@ export function authorizeHostdOperation(
case 'ping':
case 'bastille-list':
case 'pkg-version':
case 'pkg-audit':
case 'freebsd-update-status':
case 'freebsd-version':
return { allowed: true };
case 'service-start':

View file

@ -466,6 +466,42 @@ describe('handleOp — spawnSync args (RC services)', () => {
expect.any(Object),
);
});
it('pkg-audit runs pkg audit -F on the host by default', () => {
handleOp('pkg-audit', {});
expect(mockSpawnSync).toHaveBeenCalledWith(
'pkg',
['audit', '-F'],
expect.any(Object),
);
});
it('pkg-audit dispatches via bastille cmd when jail is provided', () => {
handleOp('pkg-audit', { jail: 'cms' });
expect(mockSpawnSync).toHaveBeenCalledWith(
'bastille',
['cmd', 'cms', 'pkg', 'audit', '-F'],
expect.any(Object),
);
});
it('freebsd-update-status uses updatesready (no fetch, no install)', () => {
handleOp('freebsd-update-status', {});
expect(mockSpawnSync).toHaveBeenCalledWith(
'freebsd-update',
['updatesready'],
expect.any(Object),
);
});
it('freebsd-version reports kernel + running + userland', () => {
handleOp('freebsd-version', {});
expect(mockSpawnSync).toHaveBeenCalledWith(
'freebsd-version',
['-kru'],
expect.any(Object),
);
});
});
describe('handleOp — spawnSync args (packages + sysrc + sanoid)', () => {

View file

@ -374,6 +374,26 @@ const OPS: Record<string, OpEntry> = {
handler: () => exec('pkg', ['version', '-vRUL=']),
},
// ── Read-only update audits ──────────────────────────────────────────────
// These let sysadmin (and any tenant agent diagnosing its own jail) answer
// "any updates available?" without root. None of them mutate state.
'pkg-audit': {
schema: z.object({ jail: jailName.optional() }),
handler: (p) => p.jail
? exec('bastille', ['cmd', String(p.jail), 'pkg', 'audit', '-F'])
: exec('pkg', ['audit', '-F']),
},
'freebsd-update-status': {
// updatesready exits 0 (none ready) or 2 (ready) — no fetching, no
// installing. The daily cron at 06:50 handles actual fetch+install.
schema: z.object({}),
handler: () => exec('freebsd-update', ['updatesready']),
},
'freebsd-version': {
schema: z.object({}),
handler: () => exec('freebsd-version', ['-kru']),
},
'pkg-upgrade': {
schema: z.object({ repo: pkgRepo, dryRun: z.boolean().optional() }),
handler: (p) => {