run method
Runs this command.
The return value is wrapped in a Future if necessary and returned by
CommandRunner.runCommand.
Implementation
@override
Future<void> run() async {
final args = argResults!;
// Either a directory the Hub reloads on renewal, or a static cert/key pair.
final tlsDir = validateHubTls(args);
final context = tlsDir != null
? null
: (SecurityContext()
..useCertificateChain(args['cert'] as String)
..usePrivateKey(args['key'] as String));
final grants = _parseGrants(args['grant'] as List<String>);
final nodePath = args['node-path'] as String;
final shellPath = args['shell-path'] as String;
final withShell = args['shell'] as bool;
// The Hub persists by default, in <OMNYSERVER_HOME>/hub. Only --ephemeral
// opts out — and it says so out loud, because a Hub that keeps nothing
// forgets the fleet, the audit trail and every credential it issued, on
// every restart. An explicit --data-dir is used as given.
final dataDir = resolveHubDataDir(args);
final persistent = dataDir != null;
final grantStore = persistent
? JsonGrantRepository(dataDir)
: MemoryGrantRepository();
final hub = OmnyServerHub(
HubConfig(
host: args['host'] as String,
port: int.parse(args['port'] as String),
nodeMount: nodePath,
shellMount: shellPath,
securityContext: context,
tlsDirectory: tlsDir,
// Two sources of credentials, tried in order: the ones baked into this
// command line, then the ones the Hub has issued at runtime. That is what
// lets a Hub be bootstrapped from flags and then hand out (and take back)
// credentials without a restart.
authenticator: CompositeAuthenticator([
TokenAuthenticator(grants),
GrantAuthenticator(grantStore),
]),
grantRepository: grantStore,
nodeRepository: persistent ? JsonNodeRepository(dataDir) : null,
presetRepository: persistent ? JsonPresetRepository(dataDir) : null,
formulaRepository: persistent ? JsonFormulaRepository(dataDir) : null,
auditRepository: persistent ? JsonAuditRepository(dataDir) : null,
metricRepository: persistent ? JsonMetricRepository(dataDir) : null,
desiredStateRepository: persistent
? JsonDesiredStateRepository(dataDir)
: null,
corsOrigins: args['cors-origin'] as List<String>,
alertRules: [
for (final raw in args['alert'] as List<String>) AlertRule.parse(raw),
],
logger: stdout.writeln,
),
);
// An OmnyShell broker on the same listener, sharing the Hub's credentials —
// so one Hub serves both fleets and `omnyshell node start --hub …/shell`
// just works. It authenticates in band, so it takes no connection
// authenticator; OmnyServer's own handshake stays on the node route.
omnyshell.AiConfig? aiConfig;
if (withShell) {
// The AI provider the broker proxies for the web dashboard's :ai — key
// injected on the Hub so no browser holds it. Null when unconfigured.
aiConfig = omnyshell.AiConfigIo.load(
path: omnyServerAiConfigPath(args['ai-config'] as String?),
);
final shell = ShellHub.fromGrants(
grants,
mount: shellPath,
logger: stdout.writeln,
aiConfig: aiConfig,
);
hub.registerService(shell.service());
}
// One listener, two surfaces: nodes upgrade to a WebSocket on `nodePath`,
// operators call the REST API on the same host and port. The API rides the
// Hub's TLS instead of a second plaintext socket.
final events = EventAggregator()..attach(hub.config.eventBus);
final metrics = HubMetrics(hub.registry)..attach(hub.config.eventBus);
final api = HttpApiServer(
hub: hub,
apiToken: args['api-token'] as String?,
events: events,
metrics: metrics,
);
// CORS goes on the *outermost* layer: a browser must be able to read a 401
// or a 404 (they are rendered above ordinary middleware, which would
// therefore never stamp them), and a preflight arrives with no credentials
// and must be answered before the authenticator rejects it.
final corsMiddleware = api.corsMiddleware();
if (corsMiddleware != null) hub.useOuter(corsMiddleware);
for (final middleware in api.buildMiddleware()) {
hub.use(middleware);
}
for (final service in api.buildServices()) {
hub.registerService(
service,
authenticator: service.name == HttpApiServer.apiServiceName
? api.tokenAuthenticator()
: null,
);
}
await hub.start();
final host = args['host'] as String;
stdout.writeln('Hub nodes: wss://$host:${hub.port}$nodePath');
if (withShell) {
stdout.writeln('Hub shell: wss://$host:${hub.port}$shellPath');
}
stdout.writeln('Hub API: https://$host:${hub.port}/api/v1');
stdout.writeln(
persistent
? 'Hub data: $dataDir'
: 'Hub data: in memory (--ephemeral) — a restart forgets the fleet.',
);
if (withShell) {
stdout.writeln(
aiConfig == null
? 'Hub AI: not configured — the dashboard\'s :ai has no default '
'(run: omnyserver ai config).'
: 'Hub AI: ${aiConfig.provider.wireName} — proxying :ai for web '
'clients (the key stays on the Hub).',
);
}
// Installing no CORS at all is the correct behaviour for a Hub with no
// browser client, and indistinguishable — from the browser's side — from a
// Hub that rejected the origin. Say which it is, or the next person debugs
// it from a browser console. A wildcard says so too: it is a widening, and
// one nobody should discover by reading the flags months later.
final corsOrigins = args['cors-origin'] as List<String>;
if (corsOrigins.isEmpty) {
stdout.writeln(
'Hub CORS: no origins configured — a browser dashboard will be '
'blocked (pass --cors-origin).',
);
} else if (corsOrigins.any(HttpApiServer.isAnyOrigin)) {
stdout.writeln(
'Hub CORS: any origin allowed (*) — any page may call this API. It '
'still needs a token; the browser supplies none of its own.',
);
}
// The API is controlled with the --api-token or a --grant pair, and nothing
// else. With neither, every request is refused — which is the safe failure,
// but a silent one: the Hub would look healthy and answer nobody.
if (args['api-token'] == null && grants.isEmpty) {
stdout.writeln(
'Hub AUTH: no --api-token and no --grant — the HTTP API can '
'authenticate nobody. Every call will be a 401.',
);
}
stdout.writeln('Press Ctrl-C to stop.');
await _awaitSignal();
await hub.close();
}