run method

  1. @override
Future<void> run()
override

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();
}