Skip to content

fix: declare the capabilities the modules actually use - #3

Merged
Ch4s3 merged 4 commits into
mainfrom
claude/capability-ceiling-fix
Aug 11, 2026
Merged

Ch4s3 merged 4 commits into
mainfrom
claude/capability-ceiling-fix

Conversation

@Ch4s3

@Ch4s3 Ch4s3 commented Aug 11, 2026 •

Copy link
Copy Markdown
Contributor

Unblocks every consumer that resolves conduit from main against current march main.

Why

march main enforces capability declarations, and conduit declares almost none. forgepm depends on conduit as a git dep on main and its CI typechecks against march main, so it currently fails on conduit's errors before reaching any of its own code.

March runs two capability checks, not one

This took several CI rounds to establish, and it is the useful finding here:

  1. Typecheck-time — function body calls a builtin that requires Cap(X) but <module> does not declare needs X. Reported per call site.
  2. Codegen-time — CAPABILITY CEILING: every module's emitted code must stay within its own needs. Stricter, reported per module, and it catches capabilities that reach a module through code it emits rather than through a literal builtin call in its own body. This is where all the IO.Mut violations came from.

A module can satisfy (1) and still fail (2). forge build runs the first; forge test runs both.

What changed

29 needs lines across 15 modules and the two test modules. Every line is exactly what the compiler named — none is wider.

capability modules
IO.Spawn CronScheduler, Pruner, Worker, Dashboard, Node
IO.Clock DeadLetter, Config, Queue, Worker, API, RateLimiter, WorkflowContext, WorkflowRunner
IO.Process Node
IO.Console Forge.CmdConduit
IO.Mut CronRegistry, EventStore, Node, Queue, QueueControl, Shutdown, Storage.Postgres, WorkflowContext, WorkflowRegistry
test modules ConduitTest (IO.Random, IO.Clock, IO.Mut, IO.Process), ConduitPostgresLiveTest (IO.Clock, IO.Process)

No behaviour change — these declare authority the code already exercises.

Verification

CI green on macos-14 and ubuntu-24.04. Locally: 242 tests, 0 failures.

Also verified downstream: typechecking forgepm against march main with this branch plus bastion#7 is clean, zero errors. Without them, forgepm's CI reports these capability errors plus 5 bastion Bytes type errors from march#247.

One thing deliberately left out

lib/conduit/rate_limiter.march:50 has an ambiguous Custom constructor (Conduit.Custom vs Compress.Gzip.Custom). It does not reproduce on CI's toolchain, only on an older local build, and there is already a fix for it authored on another branch. Left alone rather than duplicated here.

Ch4s3 added 4 commits August 10, 2026 22:01
march main enforces the capability ceiling on IO.Spawn, IO.Clock and
IO.Process, and nine modules here call builtins requiring them without a
`needs` declaration. Each declaration below is exactly what the compiler
reported, nothing wider:

  CronScheduler  IO.Spawn     DeadLetter  IO.Clock
  Config         IO.Clock     Pruner      IO.Spawn
  Queue          IO.Clock     Worker      IO.Spawn, IO.Clock
  Dashboard      IO.Spawn     Node        IO.Spawn, IO.Process
  API            IO.Clock

This breaks every downstream consumer that resolves conduit from main, which
is how forgepm depends on it: forgepm's CI typechecks against march main and
fails on these eleven errors before it reaches its own code.

Verified by typechecking forgepm against march main with this branch and
bastion's Bytes fix in place — clean, no errors.
The first version of this branch left this one out, on the strength of a local
check that reported it as a warning. Conduit's own CI reports it as an error
and fails the build — the difference is toolchain: CI resolves march-version
`main` (cbb8346e at time of writing), which is ahead of the build I checked
against.

The module prints progress while copying migrations, so the declaration is
straightforwardly correct regardless.
`forge build` passes with the library declarations, but `forge test` compiles
the test module too, and ConduitTest calls random_bytes in 130 places without
declaring the capability.

Verified: 242 tests, 0 failures.
March runs two separate capability checks. The typecheck-time one ("function
body calls a builtin that requires Cap(X)") is what the earlier commits on
this branch addressed. There is a second, stricter check at codegen —
"CAPABILITY CEILING: every module's emitted code must stay within its own
`needs`" — which reported 17 further violations, mostly IO.Mut.

The local toolchain I had been verifying against does not run that check, so
each CI round surfaced one more layer. This declares all 17 at once:

  IO.Mut      CronRegistry, EventStore, Node, Queue, QueueControl, Shutdown,
              Storage.Postgres, WorkflowContext, WorkflowRegistry, ConduitTest
  IO.Clock    RateLimiter, WorkflowContext, WorkflowRunner, ConduitTest,
              ConduitPostgresLiveTest
  IO.Process  ConduitTest, ConduitPostgresLiveTest

Every line is exactly what the check named; none is wider. 242 tests, 0
failures.
@Ch4s3
Ch4s3 merged commit 9a4aa30 into main Aug 11, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant