Cluster Source Contract Enforcement
Cluster Source Contract Enforcement
Section titled “Cluster Source Contract Enforcement”@requires(cap: [...]) on grain declarations is now executable compiler
policy, not only placement metadata.
For the current :cluster local-grain slice, janus build checks calls inside
the grain body against the grain’s declared capability symbols. A grain that
declares .network can call APIs requiring CapNetRead or CapNetWrite. A
grain that only declares .storage_nvme cannot call a CapNetRead API.
func read_socket() requires CapNetRead doend
@requires(cap: [.network])grain NetGrain(msg: NetMsg) do receive do NetMsg.Ping => do read_socket() end, else => do end, endendThe enforcement is intentionally local and compile-time. NexusOS placement
against NodeManifest.capabilities, migration checks, and cross-node routing
remain runtime layers. The compiler now protects the source contract before a
grain can enter those layers. The annotation shape is closed too:
@requires accepts cap: [...], not rival spellings such as caps: [...];
malformed metadata fails with E_CLUSTER_REQUIRES.
The same compiler pass now rejects alloc[Local.Shared](...) inside a grain:
grain BadStore(msg: NetMsg) do receive do NetMsg.Ping => do let slot = alloc[Local.Shared](0 as u64) // E_CLUSTER_MEMTAG _ = slot end, else => do end, endendVolatile.Ephemeral grain memory now has the matching visible-rebuild rule.
alloc[Volatile.Ephemeral](...) is legal only when the grain declares
reconstruct():
grain ScratchStore(msg: NetMsg) do reconstruct() do end
receive do NetMsg.Ping => do let scratch = alloc[Volatile.Ephemeral](0 as u64) _ = scratch end, else => do end, endendThe semantic unit tests cover the accepted reconstruct() case. The AOT
test-cluster-memory-tags gate covers the executable rejection paths:
Local.Shared inside a grain, and Volatile.Ephemeral without
reconstruct(). Full replication, passivation, and migration behavior still
belongs to the runtime slices, but the source-level invariants are enforced
before lowering.
The same source-contract pass also makes @mailbox policy honest. The v1 local
runtime implements bounded non-blocking reject semantics:
@mailbox(capacity: 4, overflow: .reject)actor Worker(msg: WorkMsg) do receive do WorkMsg.Ping => do end, endendOmitting overflow keeps the same .reject behavior. Policies such as
.drop_oldest, .drop_newest, and .block_sender now fail with
E_CLUSTER_MAILBOX until those policies are executable in the runtime. The
annotation is also closed over its canonical fields: only capacity and
optional overflow are accepted, so typos such as prefetch: fail before
lowering instead of becoming inert metadata.
Arena lifecycle metadata is now checked the same way. @arena is executable
source contract, not ignored annotation text. If an actor or grain declares an
arena, the declaration must name the matching scope, a known reset mode, and any
optional byte ceiling as a positive compile-time literal in the current v1
surface:
@arena(scope: .actor, reset: .on_restart, max_bytes: 4096)actor Worker(msg: WorkMsg) do receive do WorkMsg.Ping => do end, endendFor grains, scope must be .grain:
@arena(scope: .grain, reset: .on_deactivate)grain User(id: u64, msg: UserMsg) do receive do UserMsg.Ping => do end, endendScope mismatches, unknown reset modes, zero or non-literal max_bytes, unknown
fields, and reset: .manual without a reason now fail with
E_CLUSTER_ARENA.
For compiler-generated local actors and grains, max_bytes is also executable
for the generated scalar state-slot allocation. The compiler forwards the
literal into the local start helper; setup fails before activation if the
generated u64 state slots need more bytes than the ceiling allows. That keeps
the current runtime boundary honest without pretending that arbitrary
actor-local allocations are fully accounted yet.
let ref = Worker_start_supervised_ref(system, 0 as u64, cluster.POLICY_PERMANENT)let limit = cluster.local_ref_arena_max_bytes(ref)Raw local-system code can inspect the same configured ceiling by slot:
let limit = cluster.local_arena_max_bytes(system, 0 as u64)Full allocator-domain enforcement beyond compiler-generated scalar state slots remains a later runtime slice.
Reduction metadata now has the same canonical-form gate. The accepted source
shape is @reductions(limit: N) or
@reductions(limit: N, yield: .loop_backedge). The older budget field is not
a synonym and fails with E_CLUSTER_REDUCTIONS; invalid limits, unsupported
yield policies, and unknown fields fail the same way. The compiler validates
the policy metadata now. Full reduction-check insertion remains runtime/compiler
scheduling work.
Reload metadata is now validated as source contract too. The accepted boundary
values are .message, .idle, .supervised_restart, and .forbidden.
state and migrate must appear together. Unknown fields and invented
boundaries fail with E_CLUSTER_RELOAD. This validates dispatch-table metadata;
signed module loading, ABI/state hash comparison, dispatch-entry swap, and
Cap.cluster.hot_reload authorization remain runtime work.
Observation metadata now uses the SPEC-021 runtime-status shape:
@observe(mailbox: .summary, state: .none, current_message: .type_only).
mailbox, state, and current_message are the only accepted fields.
The older events field is rejected with E_CLUSTER_OBSERVE; lifecycle events
belong to lifecycle and tombstone streams, not observation-level metadata.
Tombstone metadata now follows the explicit hot-index policy shape:
@tombstone(digest_includes: [.payload], retention_window: 60_000, deadly_threshold: 3).
The accepted fields are enabled, digest_includes, retention_window, and
deadly_threshold. The older classifier field is rejected with
E_CLUSTER_TOMBSTONE; deterministic-deadly behavior is derived from the
threshold, retention window, and match key.
Behaviour metadata now has a build-time shape gate. @behaviour(.server)
requires a visible init hook in the v1 source contract, unknown behaviour
symbols fail, and .supervisor is routed to supervisor ... end syntax rather
than actor/grain annotations. Shape violations report CL-E1413.
Grain persistence and lifecycle metadata are also executable source contract
now. @persist(via: GrainStoreBytes) is the only v1 persistence substrate and
is valid only on grains; actor use, unknown fields, missing via, or future
store names fail with E_CLUSTER_PERSIST. @lifecycle is grain-only, requires
activation: .lazy, and accepts either omitted deactivation metadata, .never,
or .idle_timeout(ms) with a positive compile-time millisecond literal. Invalid
lifecycle metadata fails with E_CLUSTER_LIFECYCLE.
Replication metadata is checked as Phase-B source contract as well. The
accepted shapes are @replicate(scope: .wing),
@replicate(scope: .cluster), and
@replicate(scope: .swarm, protocol: .pbft). Runtime replication still belongs
to the cluster runtime, but unsupported source metadata now fails with
E_CLUSTER_REPLICATE before lowering.
Verified gates:
./scripts/zb test-capability-requires./scripts/zb test-cluster-persist-policy./scripts/zb test-cluster-lifecycle-policy./scripts/zb test-cluster-grain-requires./scripts/zb test-cluster-memory-tags./scripts/zb test-cluster-replicate-policy./scripts/zb test-cluster-mailbox-policy./scripts/zb test-cluster-arena-policy./scripts/zb test-cluster-arena-runtime./scripts/zb test-cluster-reductions-policy./scripts/zb test-cluster-reload-policy./scripts/zb test-cluster-observe-policy./scripts/zb test-cluster-tombstone-policy./scripts/zb test-cluster-behaviour-policy./scripts/zb test-cluster-actors