Motivation
In orca we split side-effect gating into capability tokens — a shared one that may legally cross fork boundaries (LLM-call gate, caps.SharedCapability) and exclusive ones that must not (workspace/git-index writers, caps.ExclusiveCapability) — and we want Scala's capture + separation checking to reject an exclusive capability captured into a fork closure at compile time.
That works today, but only through a wrapper, because of an empirically verified limitation: separation checking does not fire through combinators of a library that was not itself compiled under capture checking. Calling ox.fork or ox.flow.Flow.mapParUnordered from a CC-enabled file yields zero enforcement — the checker needs the combinator's own signature to declare what the closures may capture.
Our bridge is CheckedPar — a 1:1 delegate to Flow.mapParUnordered, compiled under captureChecking + separationChecking, whose capture-set parameter (thunks: Seq[() ->{C} T] with C^) makes the call site checkable. With it, a closure capturing an exclusive capability is a hard "Separation failure" compile error while shared capabilities pass — both directions pinned by a dotc-in-test negative-compile suite. The wrapper is explicitly a bridge designed for deletion once Ox carries the annotations natively (design record: ADR 0018 §6).
What Ox would need
- Capture-aware signatures on the closure-accepting concurrency API —
fork/forkUser/etc., supervised/unsupervised bodies, and the Flow parallel combinators (mapPar, mapParUnordered, …), so separation checking evaluates each caller's closure captures at the call site. In our experiments, plain =>-style params on a CC-compiled combinator suffice for the hidden-set rules to engage; an explicit capture-set type parameter (as in CheckedPar) gives callers something to bind.
- Track the
Ox capability itself — so a fork handle or Ox-scoped resource escaping its supervised scope becomes a compile error. Downstream this would also let us make "agent value outlives its owning scope" a compile error rather than a runtime latch.
Findings from our adoption that may save you time (all verified on stock Scala 3.8.4, no nightly, no flags)
caps.Capability is sealed; caps.SharedCapability is non-experimental (extendable with zero language imports), caps.ExclusiveCapability is @experimental (defining files need the CC import).
- No consumer taint for definitions: a jar built from CC-importing files is consumable by plain non-CC consumers with zero flags. But a signature carrying a capture-set parameter (
C^) is only callable from CC-enabled compilation units — an API-surface consequence worth designing around (it's why CheckedPar is deliberately not on orca's exported surface).
- Enforcement locus is the caller's compilation unit: both the combinator and each calling file need the language imports for violations to be caught.
- Tooling caveats: scalafmt (3.8.3) cannot tokenize the
^ syntax (per-file exclusion needed); tapir's derives schema derivation doesn't typecheck under CC (keep macro-deriving types out of CC-compiled files); munit's compileErrors/typeCheckErrors are blind to CC violations (CC runs post-typer) — negative tests need separate compilation.
Happy to share more details from the adoption or test fixtures if useful.
🤖 Generated with Claude Code
Motivation
In orca we split side-effect gating into capability tokens — a shared one that may legally cross fork boundaries (LLM-call gate,
caps.SharedCapability) and exclusive ones that must not (workspace/git-index writers,caps.ExclusiveCapability) — and we want Scala's capture + separation checking to reject an exclusive capability captured into a fork closure at compile time.That works today, but only through a wrapper, because of an empirically verified limitation: separation checking does not fire through combinators of a library that was not itself compiled under capture checking. Calling
ox.forkorox.flow.Flow.mapParUnorderedfrom a CC-enabled file yields zero enforcement — the checker needs the combinator's own signature to declare what the closures may capture.Our bridge is
CheckedPar— a 1:1 delegate toFlow.mapParUnordered, compiled undercaptureChecking+separationChecking, whose capture-set parameter (thunks: Seq[() ->{C} T]withC^) makes the call site checkable. With it, a closure capturing an exclusive capability is a hard "Separation failure" compile error while shared capabilities pass — both directions pinned by a dotc-in-test negative-compile suite. The wrapper is explicitly a bridge designed for deletion once Ox carries the annotations natively (design record: ADR 0018 §6).What Ox would need
fork/forkUser/etc.,supervised/unsupervisedbodies, and theFlowparallel combinators (mapPar,mapParUnordered, …), so separation checking evaluates each caller's closure captures at the call site. In our experiments, plain=>-style params on a CC-compiled combinator suffice for the hidden-set rules to engage; an explicit capture-set type parameter (as inCheckedPar) gives callers something to bind.Oxcapability itself — so a fork handle orOx-scoped resource escaping itssupervisedscope becomes a compile error. Downstream this would also let us make "agent value outlives its owning scope" a compile error rather than a runtime latch.Findings from our adoption that may save you time (all verified on stock Scala 3.8.4, no nightly, no flags)
caps.Capabilityis sealed;caps.SharedCapabilityis non-experimental (extendable with zero language imports),caps.ExclusiveCapabilityis@experimental(defining files need the CC import).C^) is only callable from CC-enabled compilation units — an API-surface consequence worth designing around (it's whyCheckedParis deliberately not on orca's exported surface).^syntax (per-file exclusion needed); tapir'sderivesschema derivation doesn't typecheck under CC (keep macro-deriving types out of CC-compiled files); munit'scompileErrors/typeCheckErrorsare blind to CC violations (CC runs post-typer) — negative tests need separate compilation.Happy to share more details from the adoption or test fixtures if useful.
🤖 Generated with Claude Code