better/compute

Overview

Every seam of Better Compute is a typed contract you register through options. What each extension point is for, where it runs and how stable it is.

Better Compute has no plugin loader. Every place you can change its behaviour is a typed contract in @better-compute/core, and you plug an implementation in by passing it to betterCompute(). Two rules from PLAN.md §5.1 shape all of them:

  • Code registers kinds; data configures instances. A driver, a provider or an attestor is code you register once. A BMC binding, a provider account or a join token is a row, validated by the manifest that code declares.
  • Registration is explicit. Nothing is discovered from the file system or node_modules. Everything arrives in an options array, and the registry refuses what it cannot check: a duplicate kind:id@version, a method whose action is missing from the IAM catalog, or a migration named for another module are all INVALID_CONFIG when betterCompute() or initialize() runs.

The extension points

Extension pointDeclared withImport fromRegistered throughRuns in
ModuledefineModulebetter-computemodulescontrol plane
Plugin (a module with an id)definePluginbetter-computepluginscontrol plane
API methoddefineMethodbetter-compute/corea module's api()control plane
DriverdefineDriverbetter-computedrivers, or a module's extensionscontrol plane, site agent, node, ramdisk
Out-of-process driver (bcx)serveDriver, externalDriver@better-compute/driver-sdkdriversits own process
AttestordefineAttestorbetter-computeattestorsJoinService, in the control plane
ProviderdefineProviderbetter-compute/vpsproviderscontrol plane
Scheduler plugindefineSchedulerPluginbetter-computescheduler.plugins, or a module's extensionsthe placer, in api and worker processes
Event sinkdefineSink({ kind: 'events' })better-computesinks.events, or a module's extensionsthe sinks worker role
Audit sinkdefineSink({ kind: 'audit' })better-computesinks.audit, or a module's extensionsthe audit-sinks reconciler, controllers role
Step and profiledefineStep, defineProfilebetter-compute/provisiona module's extensions and profilescontrol plane or one agent command

The root entry better-compute exports defineModule, definePlugin, defineDriver, defineSchedulerPlugin, defineAttestor and defineSink. defineMethod, defineStep, defineProfile and defineProvider come from the better-compute/core, /provision and /vps subpaths.

Other contracts in core are ports rather than plug-ins: SqlExecutor (the store), IamPort, Clock, Notifier, LeaseProvider, SecretStore, BlobStore, CertificateAuthority, ImageSource and NodeAdapter. You replace them through their own options (database, iam, clock, notifier, secrets, pki), not through extensions; the configuration reference lists them.

Where each one plugs in

A module is the unit everything else hangs off. It can carry API groups, reconcilers, migrations, command kinds, NodeSpec sections, IAM catalog fragments, CLI commands, profiles and extensions (drivers, steps, scheduler plugins, attestors, sinks). The top-level options (drivers, providers, attestors, scheduler.plugins, sinks) register extensions that do not need a module of their own. Both paths end in the same registry and the same kind:id@version uniqueness check, and every consumer reads both: scheduler plugins a module lists join scheduler.plugins in the placer, and its event and audit sinks join sinks.events and sinks.audit.

compute.ts
import { betterCompute, defineSchedulerPlugin, defineSink } from 'better-compute';
import { acmeWidgets } from '@acme/bc-widgets'; // definePlugin({ id: 'acme-widgets', … })

export const compute = betterCompute({
  // …database, secret, baseURL, iam
  plugins: [acmeWidgets()],
  scheduler: { plugins: [defineSchedulerPlugin({ id: 'rack-spread', scorers: [/* … */] })] },
  sinks: {
    events: [defineSink({ kind: 'events', id: 'warehouse', async deliver(events) { /* … */ } })],
  },
});

What is stable

Better Compute is pre-1.0. The contracts in @better-compute/core froze at the end of phase P0 and change only by additive edits: new optional members in minor versions, and a new interface selected by version for anything breaking. PLAN.md §5.1 keeps every define* helper experimental until v1.0, so pin your versions.

Extension pointState today
Modules, plugins, methodsBuilt. Every built-in feature (capacity, events, fleet, rollout, audit, iam, vps, metal, provision) is a module on this contract.
Reconcilers, command kinds, NodeSpec sections, migrationsBuilt, with the v0.1 worker runtime.
Scheduler pluginsBuilt: filters, scorers, atom pickers and admission hooks run in both placers.
SinksBuilt: event sinks on OutboxDispatch with durable cursors and dead letters; audit sinks on the built-in audit-sinks module. Usage sinks are refused with INVALID_CONFIG (usage goes to IAM billing through the bridge); see sinks. Sinks and scheduler plugins a module lists in extensions run like those in the options.
AttestorsBuilt: join-token, x509pop, provisioned, discovery, cloud-iid and tpm are all defineAttestor values. bcd produces join-token joins, provisioned joins for an OS a deploy installed (bcd join --node-id), discovery joins from bcd ramdisk with its site agent's signed assertion, and tpm joins with the host's TPM (bcd join --attestor tpm, 8545688); its producers for x509pop and cloud-iid are not written.
Drivers, steps, profilesBuilt in TypeScript (metal, provision). bcd runs the metal command kinds that steps send (Redfish BMC operations, boot entries and site.httpProxy on bcd site; wipes, image writes, kexec and DCGM on bcd ramdisk), and driver.invoke in the bcx plugins it hosts; not released and tested by Go unit tests only.
ProvidersBuilt: hetzner and proxmox provider packages, checked by providerConformanceCases().
bcx out-of-process driversA v0.5 draft. @better-compute/driver-sdk works for TypeScript plugins, and bcd hosts sha256-pinned plugins (see drivers); the proto is unstable until v0.5 ships.

The daemon half is mostly missing

Several extension points end in a command that a agent runs: driver operations on a site agent, steps on a ramdisk, NodeSpec sections on a node. The Go daemon's node agent is built and tested end to end, and its site-agent and ramdisk modes are written with Go unit tests; none of it is released. It converges the devices, access, instances (on libvirt-kvm with virsh or cloud-hypervisor with its binary, both tested against fake tools only, or a fakehw build's file-backed runtime), site and daemon sections and runs its built-in and metal kinds, driver.invoke in the bcx plugins it hosts, and on a site agent bmc.* operations through a hosted BMC driver such as bcx-ipmi, but no host section. The TypeScript FakeDaemon in @better-compute/testing speaks the real protocol and runs the other paths in tests and in better-compute dev --sim N. See what bcd does today.

Rules every extension follows

These hold for anything you write against the contracts, and the built-in modules follow them too:

  1. Modules never import each other's internals. They meet through the ports on ModuleContext: the vps module books host capacity through ctx.capacity.reserve, not by importing @better-compute/capacity. The repository enforces the direction with scripts/check-deps.mjs.
  2. IAM is never awaited inside a compute transaction. The envelope makes the one decision before your handler's transaction opens. Composite methods decide their sub-calls with ctx.check before they open a transaction.
  3. Write state, events and work in one transaction. ctx.events.publish, ctx.work.enqueue, ctx.commands.enqueue and ctx.authz.upsert all take the caller's tx, so a rollback leaves nothing behind.
  4. Business time comes from the database. SQL uses compute_now(), never now(); TypeScript uses ctx.clock. scripts/check-sql-now.mjs fails CI on a bare now() in packages/*/src.
  5. Everything is retried. Reconcilers are level-triggered, sinks are at-least-once, commands carry idempotency keys. Write each piece so that running it twice changes nothing.

Conformance

A contract comes with a conformance suite: plain { name, run } cases you run against your implementation. The runner lives on its own subpath of @better-compute/core, so the root entry carries no test code:

acme-cloud.test.ts
import { runConformance } from '@better-compute/core/conformance';
import { providerConformanceCases } from 'better-compute/vps';
import { acmeCloudSubject } from './acme-cloud-subject'; // your provider, against recorded responses

const report = await runConformance(providerConformanceCases(), () => acmeCloudSubject());
// report: { passed: string[], failed: { name, error }[] }; a factory gives each case a fresh subject

storeConformanceCases() from @better-compute/postgres/conformance does the same for a SqlExecutor. The acceptance tests in tests/extensions/ register third-party-style plugins with zero edits to @better-compute/*; see walkthroughs.

Was this page helpful?

Better Compute is created by Sean Filimon

Last updated

On this page