feat: graduate Host Console to Community admins (#1669)

* feat: graduate Host Console to Community admins

Make Host Console available to Community and Admiral admins (system:console), add host-console-community for mixed fleets, and keep opaque API tokens off the host shell.

* docs: document Host Console deep links

Cover root and stack-scoped Console URLs, correct the phone treatment note, and pin parse/build round-trips in senchoRoute tests.

* fix: bind Host Console socket to the resolved node

Treat unresolved activeNode as loading, target the WebSocket with an explicit nodeId, and wait for stack deep-link hydration so the shell cannot open on the wrong node or compose root. Add regression coverage for node/stack retargeting and fail-closed directory resolution.

* fix: harden Host Console node binding, audit acting_as, and console_session tokens

Reject unknown or malformed nodeIds before spawning a PTY. Record hub operators in audit_log.acting_as for remote console_session bridges. Path-scope and one-time-consume console_session JWTs so Host Console mints cannot open container exec or be replayed.

* test: expect acting_as in audit CSV export header

Align the CSV export assertion with the P0-2B acting_as column added to audit log exports.
This commit is contained in:
Anso
2026-07-23 12:59:53 -04:00
committed by GitHub
parent ed5ca9c4f6
commit dd54a2e483
43 changed files with 1230 additions and 199 deletions
+14 -14
View File
@@ -49,25 +49,25 @@ describe('reachability', () => {
expect(isViewHidden('fleet', noFleet)).toBe(true);
});
it('preserves paid views when license metadata failed', () => {
const licenseError = ctx({ licenseStatus: 'error', experimental: true });
it('preserves host-console when authz is not ready', () => {
const licenseError = ctx({ licenseStatus: 'error', can: (a) => a === 'system:console' });
expect(isViewHidden('host-console', licenseError)).toBe(false);
});
it('does not apply experimental hide to host-console until experimentalReady', () => {
const loading = ctx({ experimental: false, experimentalReady: false, isPaid: true, isAdmin: true });
expect(isViewHidden('host-console', loading)).toBe(false);
it('hides host-console without system:console when ready', () => {
const noConsole = ctx({ can: () => false, isPaid: false, experimental: false });
expect(isViewHidden('host-console', noConsole)).toBe(true);
expect(normalizeHiddenView('host-console', noConsole)).toBe('dashboard');
});
it('hides host-console when experimental is ready and off even for paid admin', () => {
const off = ctx({ experimental: false, experimentalReady: true, isPaid: true, isAdmin: true });
expect(isViewHidden('host-console', off)).toBe(true);
expect(normalizeHiddenView('host-console', off)).toBe('dashboard');
});
it('keeps host-console when experimental is on for paid admin', () => {
const on = ctx({ experimental: true, experimentalReady: true, isPaid: true, isAdmin: true });
expect(isViewHidden('host-console', on)).toBe(false);
it('keeps host-console for system:console regardless of tier or experimental', () => {
const community = ctx({
isPaid: false,
experimental: false,
experimentalReady: true,
can: (a) => a === 'system:console',
});
expect(isViewHidden('host-console', community)).toBe(false);
});
it('hides routing and secrets fleet tabs only after experimentalReady when off', () => {