external-consumer/

directory
v0.2.4 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 3, 2026 License: Apache-2.0

README

Building against candace from another repository

This is a complete Bazel repository that depends on candace the way a real consumer does: it pins one published source archive and, from that archive alone, links a Core binary of its own wearing its own identity, serving its own page, running its own services, and driven by its own agent runtime. It never forks or vendors the source tree, and it holds every one of Core's extension seams to a compiled test.

Everything under _workspace/ is that repository. The leading underscore keeps it out of both toolchains that would otherwise claim it: the go command ignores such a directory, and .bazelignore stops Bazel from reading its BUILD.bazel files as packages of this module. They are not — they belong to a build that resolves @csf to a downloaded archive.

What it proves

Core resolves its extension points at compile time; a consumer chooses them by handing options to bootstrap.Run. Every option that composes behavior is exercised here, and every candace package behind them arrives through an @csf// label pointing at a tarball:

Seam What this repository supplies
WithComponent three components of its own, in a graph Core orders
WithHarnessFactory a full harness.IFactory and harness.IRuntime
WithBrand an invented product's name, agent, wordmark, and palette
WithUIOverlay one shipped template block, redefined
WithNavItem one sidebar entry, after Core's own four
WithHTTPService the page that entry links to

The one option missing from that table is bootstrap.WithPII, which is not a seam: it turns off Core's redaction of configuration-derived values in errors and diagnostics. Nothing a consumer writes changes with it, so this repository leaves the redaction on, as a product shipping to operators would.

The product is called Quillfern. It is invented for this example — not a real product, company, or service — and it exists so the identity seams are exercised by something other than candace's own name.

What it builds

  • steering/ — a bounded store and a service, composed with component.WithRequires so Core assembles and starts the store first and stops it last. Core constructs neither and reads neither one's configuration; it owns only the order.
  • noteboard/ — this repository's own service, and the one with business logic rather than a fixture's. It keeps a bounded ledger of the steering inputs the harness observed, treats a consecutive repeat as a retry, counts sequence numbers past an evicted note, and records nothing until Core has started it. It joins the graph as a component requiring the steering service — an edge between two of this repository's own components, resolved by Core — and it mounts its own operator page through the HTTP seam.
  • identity/ — the product identity: the two brand-bearing names, a wordmark that reuses the shipped lockup's markup so it needs no overlay asset at all, a palette delivered as a served same-origin stylesheet, and an overlay carrying exactly one file, a redefinition of the shipped "statusPill" block. Everything the overlay does not name keeps shipping from candace.
  • customharness/ — a full harness.IFactory and harness.IRuntime implementation compiled outside the deploy tree, publishing typed events through the host boundary and holding the steering service the composition root handed it.
  • composition/ — the composition root, as a library rather than inline in main, so the suites assert on the option list the binary is linked with instead of a second copy of it written to be asserted on.
  • cmd/ — bootstrap.Run with that option list, producing a Core binary with a different agent runtime, a different identity, an extra page, and the stock control plane.
  • The suites — Ginkgo specs over the harness with a gomock host, over the ledger's own rules, over the resolved component order, and over the rendered UI. The presentation specs bring this repository's graph up through Assemble and Start in the resolved order, exactly as Core would, and then render the pages on the same engine Core builds. None of them needs a Core, a PostgreSQL, or a network.

What Core keeps is as much the point: its routes, including the /claws/... paths, its snapshot contract, its API, its persistence, and every string in the UI that does not name the product or the agent. The specs assert that too.

The two ways to pin the archive

Each release of candacelabs/csf carries a csf-<sha12>.tar.gz and its .sha256. MODULE.bazel at the archive root makes the tarball a Bazel module, so there are two shapes, and this example is built both ways before any archive is published.

Public download URLs apply only once the named public release exists. For a private staging archive, download it with authenticated access and use a local archive URL or the local Go consumer.

bazel_dep + archive_override — use this one. _workspace/MODULE.archive-override.bazel.in names the module, pins the exact bytes with Subresource Integrity, and lets candace's own MODULE.bazel supply the versions of its dependency closure, register the Go SDK its committed BUILD files were generated against, and declare the repositories candace packages for itself.

bazel_dep(name = "csf", version = "0.1.0")

archive_override(
    module_name = "csf",
    integrity = "sha256-...",          # `integrity` from the packager
    strip_prefix = "csf-<sha12>",  # `strip_prefix` from the packager
    urls = ["https://github.com/candacelabs/csf/releases/download/v0.1.0/csf-<sha12>.tar.gz"],
)

http_archive — the fallback. _workspace/MODULE.http-archive.bazel.in fetches the same tarball without treating it as a module. That is sometimes what a consumer wants, and it costs the repository mapping: a repository fetched this way is not a module, so the labels inside candace's BUILD files resolve through the consumer's mapping. Every repository those files name has to be visible in the consumer's MODULE.bazel, and a missing one is a build error inside candace. The repositories candace declares for itself are the ones that cannot be supplied by pasting a use_repo block: @csf//pkg/pgmem names @pg_query_go, a patched archive candace's own MODULE.bazel fetches, so in this shape it does not resolve at all until the consumer copies that declaration too. This shape also has to download the Go SDK itself, because candace registers no toolchain when it is not a module.

Both shapes read candace's go.mod, so both carry its use_repo list. This workspace has no go.mod of its own and cannot have one — it lives inside the export root, and the published archive is exactly one Go module — so both files point go_deps.from_file at candace's. Gazelle then treats candace's direct dependencies as the consumer's, which is what bazel mod tidy writes; a shorter list, even one naming exactly the repositories this workspace's BUILD files use, makes every build print a warning telling the consumer to fix its module file. Five of the entries are named by a BUILD file here and the rest ride along. A consumer repository with a go.mod of its own points from_file at that one, and then the list is its own direct dependencies.

Both files carry @CANDACE_ARCHIVE_...@ placeholders and a @CANDACE_GO_REPOS@ line. The monorepo's tools/test_candace_external_consumer.sh substitutes them with the lock material tools/package_candace_archive.sh prints and with the use_repo block read out of the archive's own MODULE.bazel — so neither file can describe a dependency set the archive no longer has — serves the archive from a throwaway container, and then builds every target and runs every suite in this workspace, in both shapes, inside the pinned Bazel image. It fails the run if either build reports the consumer's use_repo list as incorrect. That is how a release learns that its archive works before anyone depends on it.

Running it yourself

Against a published release, copy _workspace/, write one of the two module templates as MODULE.bazel, and fill it in by hand: the release's integrity (or sha256), its strip_prefix, its archive URL, and — in place of the @CANDACE_GO_REPOS@ line — the use_repo(go_deps, ...) block from the archive's own MODULE.bazel, which bazel mod tidy will also write for you. Keep _workspace/.bazelrc too: CSF's OCaml compiler resolves tools_opam from the OBazl registry listed there, before Bazel searches the public registry. This outside-product example builds the Go runtime, so Candace does not ask it to resolve the archive's development-only compiler toolchain. Then, from inside that copy:

bazel build //...
bazel test //...

//cmd:custom-deploy is the resulting Linux Core executable, with the custom harness, the custom components, and the custom presentation compiled in. It reads exactly the configuration the stock command reads — a PostgreSQL URL, a writable data directory and workspace, a Warden URL, and a harness selection — and adds no setting of its own. infra/deploy-kit/README.md describes how a fleet deployment layers such a binary over the standard Core runtime.

Consuming from a legacy WORKSPACE build

There is a second-class path for a repository that has not migrated to bzlmod. It is documented in bazel/README.md, it is not what this example demonstrates, and no test in this repository can exercise it: the Bazel release this module pins removed WORKSPACE support entirely.

What the external consumer proves

Run from a pristine extraction of the export root, the end-to-end check establishes one checkable thing: a repository with no candace source in it builds every target it declares and passes every suite it declares, in both pinning shapes, against a tarball served over HTTP, using nothing from the monorepo but that tarball. The composition it links is the whole extension surface at once — three components in a graph Core orders, a full agent harness, an invented identity, an overlay, a sidebar entry, and a page of its own — so "a service is easy to add" is a claim the build either satisfies or breaks on.

It stops short of running Core. An assembled Core opens PostgreSQL, a Warden client, and a harness, so the suites hold the values this repository hands bootstrap.Run to the same web UI and the same Gin engine Core assembles them into, and the Core binary itself is proven by linking rather than by booting. Deploying such a binary over a fleet is infra/deploy-kit/README.md's subject, not this example's.

Directories

Path Synopsis
_workspace
cmd command
Command custom-deploy is this repository's own Core binary.
Command custom-deploy is this repository's own Core binary.
composition
Package composition is this repository's composition root: the exact set of values handed to bootstrap.Run, assembled in one place so the binary and its suite cannot describe different products.
Package composition is this repository's composition root: the exact set of values handed to bootstrap.Run, assembled in one place so the binary and its suite cannot describe different products.
customharness
Package customharness is a complete harness implementation compiled outside the deploy source tree.
Package customharness is a complete harness implementation compiled outside the deploy source tree.
identity
Package identity is this repository's own product identity: the two brand-bearing names, the lockup rendered in the shell, the design tokens the operator stylesheet reads, and the one shipped template block its overlay redefines.
Package identity is this repository's own product identity: the two brand-bearing names, the lockup rendered in the shell, the design tokens the operator stylesheet reads, and the one shipped template block its overlay redefines.
noteboard
Package noteboard is this repository's own service.
Package noteboard is this repository's own service.
steering
Package steering composes an agent-steering service alongside deploy Core.
Package steering composes an agent-steering service alongside deploy Core.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL