feat: hand a compiled plan to a node over the stream #1

Closed
day01 wants to merge %!d() commits from %!s() into %!s()
Owner

The control-plane half of the slice that runs a job without the forge. It
depends on syncode/workflow#3, which puts the plan on the wire, and is pinned
to that branch until it merges.

Protocol

proto/node.proto defines one persistent bidirectional stream per node:
NodeSession.Open. The node sends Hello, progress and logs; the control
plane sends Welcome and assignments. Nothing polls, and the control plane
numbers what it sends so a node can tell a repeat from a new decision.

The assignment carries the compiled plan as bytes — the serialized domain
model, not protobuf messages restating it, per D018. The plan carries its own
schema version, so the node refuses one it does not speak without the protocol
having to describe the plan at all.

Domain

Run is an aggregate: RunCommand in, RunEvent out, state never assigned
directly, every event numbered by a monotonic Sequence. Finish and Start
are refused from a node that does not hold the run — the place where a fencing
token goes once leases arrive in the next slice.

ControlService keeps runs and the queue in memory. That is deliberate: the
aggregate decides, and where its log lives is a question this type exists to
answer by being replaced rather than by growing.

Verification

cargo fmt --check, cargo clippy --workspace --all-targets --all-features -- -D warnings,
cargo test --workspace --all-targets --all-features, check-architecture.sh,
check-rust-loc.sh.

tests/node_session.rs starts the service on a real socket, connects as a node
would, and asserts the whole path: hello answered, the assignment carries the
plan the compiler produced, the decoded plan equals it and states the current
schema, the run knows its holder before the assignment leaves, a reported
success finishes the run, and a stranger's report is refused.

Not here yet

Enrollment and credentials, capabilities and capacity, leasing and fencing,
cancellation, matrix and reusable expansion, secrets, audit, persistence. Each
arrives with the test that proves it.

The control-plane half of the slice that runs a job without the forge. It depends on `syncode/workflow#3`, which puts the plan on the wire, and is pinned to that branch until it merges. ## Protocol `proto/node.proto` defines one persistent bidirectional stream per node: `NodeSession.Open`. The node sends `Hello`, progress and logs; the control plane sends `Welcome` and assignments. Nothing polls, and the control plane numbers what it sends so a node can tell a repeat from a new decision. The assignment carries the compiled plan as bytes — the serialized domain model, not protobuf messages restating it, per D018. The plan carries its own schema version, so the node refuses one it does not speak without the protocol having to describe the plan at all. ## Domain `Run` is an aggregate: `RunCommand` in, `RunEvent` out, state never assigned directly, every event numbered by a monotonic `Sequence`. `Finish` and `Start` are refused from a node that does not hold the run — the place where a fencing token goes once leases arrive in the next slice. `ControlService` keeps runs and the queue in memory. That is deliberate: the aggregate decides, and where its log lives is a question this type exists to answer by being replaced rather than by growing. ## Verification `cargo fmt --check`, `cargo clippy --workspace --all-targets --all-features -- -D warnings`, `cargo test --workspace --all-targets --all-features`, `check-architecture.sh`, `check-rust-loc.sh`. `tests/node_session.rs` starts the service on a real socket, connects as a node would, and asserts the whole path: hello answered, the assignment carries the plan the compiler produced, the decoded plan equals it and states the current schema, the run knows its holder before the assignment leaves, a reported success finishes the run, and a stranger's report is refused. ## Not here yet Enrollment and credentials, capabilities and capacity, leasing and fencing, cancellation, matrix and reusable expansion, secrets, audit, persistence. Each arrives with the test that proves it.
day01 added 1 commit 2026-08-05 06:13:41 +00:00
		The control plane compiles a workflow, keeps the run as an aggregate advanced
by commands, and pushes the versioned plan down a bidirectional gRPC stream.
The node says hello and is given work; nothing polls, and a report only counts
from the node that holds the run.
	
day01 added 1 commit 2026-08-05 06:53:05 +00:00
		The stream handler converted every failure at its call site. It now has one
error type with From impls and a single conversion to Status at the boundary.
	
day01 added 1 commit 2026-08-05 06:53:48 +00:00
day01 added 1 commit 2026-08-05 10:33:38 +00:00
		control-domain said nothing about its contents. The runs the control plane
holds, the aggregate that decides about them and the queue in front of them are
one crate; the gRPC stream a node opens is the other.
	
day01 added 1 commit 2026-08-05 10:39:26 +00:00
		dtolnay/rust-toolchain publishes versions as branches whose heads move, so a
commit pinned today is unreachable from any ref once the branch advances and a
fresh mirror no longer carries it. The branch is what a mirror can serve.
	
day01 added 1 commit 2026-08-05 10:42:44 +00:00
		The runner on production cannot resolve an action reference named like a
version, so dtolnay/rust-toolchain is unusable there until it ships the fix.
The toolchain is pinned by image digest instead, which the job container
resolves without any action at all.
	
day01 closed this pull request 2026-08-05 16:29:48 +00:00

Pull request closed

This pull request cannot be reopened because the branch was deleted.
Sign in to join this conversation.
No Reviewers
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: syncode/control#1