Add actor-owned contextual process provisioning, descriptor bootstrap, scoped namespace operations, Python bindings, Myelin orchestration, and adversarial end-to-end coverage. Remove the superseded job runner, pipeline APIs, images, entrypoints, and specifications.
25 KiB
Swactor Contextual Process Feature and Migration Specification
Status: Implemented
Last modified: c705c74289
This specification supersedes the job-runner design. It defines the target behavior, architecture, migration, implementation order, and verification for spawning an OS process that may claim a Swactor context.
1. Objective
Swactor MUST support two explicit forms of supervised process execution:
- Native process: the existing managed OS process, with lifecycle, termination, stdout, and stderr supervision.
- Contextual process: the same managed OS process plus a provisioned, process-scoped Swactor context that an installed language binding may claim.
Contextual execution MUST reuse swactor-process; it MUST NOT introduce a job,
job runner, scheduler, workspace protocol, setup/run phases, output collector,
or job FSM.
For Python, the guest API remains:
import swactor
async def main(ctx):
...
swactor.run(main)
swactor.run MUST obtain everything required to construct ctx without the
program supplying actor addresses, transport endpoints, capabilities, arena
metadata, or routing configuration.
2. Scope
2.1 In scope
- safe supervision of contextual OS processes;
- one opaque host-to-child bootstrap handle;
- data-plane session, arena, routing, and capability provisioning;
- exactly-once binding attachment;
- observable distinction between OS start and context readiness;
- deterministic lifecycle and cleanup under races and failures;
- migration of the existing Python binding;
- removal of the job runner and job-specific process/data-plane terminology;
- a deterministic, model-based test architecture that exercises arbitrary legal event DAGs and rejects minimally invalid variants.
2.2 Out of scope
- automatic language or entrypoint detection;
- transparent libc, allocator, syscall, CUDA, or device interception;
- making an unaware executable use a Swactor context;
- dependency installation, workspaces, setup commands, or output collection;
- scheduling, placement, retry, or node provisioning policy;
- UI submission and remote artifact transfer;
- context inheritance across
fork; - generalized interactive process support.
A contextual executable opts in by invoking a supported binding. An executable that does not invoke a binding remains valid only on the native spawn path.
3. Terminology
- Process core:
swactor-process, responsible only for OS process lifecycle. - Contextual spawner: the composition layer that provisions a context and delegates OS supervision to the process core.
- Bootstrap handle: the sole opaque descriptor inherited by a contextual child.
- Bootstrap claim: the binding's one permitted use of that handle.
- Context ready: the binding has attached successfully and can use the provisioned data plane.
- Session: the process-scoped host data-plane session.
- Session capability: the unforgeable authorization for that session.
- Session access: the execution identity and namespace prefixes authorized for that session.
- Execution: one contextual process lifetime. This is correlation identity, not a job abstraction.
4. Required behavior
4.1 Native process behavior
spawn_local_process and ProcessSpec retain their existing contract. Native
spawn MUST NOT:
- allocate an arena;
- create a data-plane session;
- inherit a bootstrap handle;
- wait for a binding;
- emit contextual lifecycle events;
- depend on the contextual-spawn crate.
4.2 Contextual process behavior
A contextual spawn MUST:
- validate its process specification and session access;
- provision the arena, session capability, host session, and private bootstrap channel;
- transfer the child end of that channel through the process core as an explicitly owned inherited resource;
- start the OS child using the existing process actor;
- allow the installed binding to claim the bootstrap exactly once;
- construct a usable binding context without caller coordination;
- resolve context readiness or bootstrap failure;
- preserve native process lifecycle facts;
- stop a started child whose context cannot become ready;
- revoke the session and release every contextual resource on every terminal path.
The contextual spawner owns this sequence. Its caller supplies normal process configuration and session authorization, not transport internals.
4.3 Guest-visible bootstrap surface
The child MUST inherit exactly one Swactor-owned bootstrap descriptor. The private binding ABI SHOULD use a fixed descriptor number so no environment variable is required.
The following environment variables MUST be removed:
SWACTOR_ARENA_FD
SWACTOR_DATA_PLANE_ACTOR
SWACTOR_JOB_CAPABILITY
SWACTOR_DATA_PLANE_ENDPOINT
The binding MUST treat the bootstrap descriptor as opaque. A one-use Unix
SOCK_SEQPACKET channel is the preferred Linux implementation. The host sends
private, versioned session material and transfers the arena descriptor with
SCM_RIGHTS only after accepting the claim.
Possession of the bootstrap descriptor is a bearer capability. The protocol does not promise to conceal its bytes from a malicious child; it promises that these bytes are not configuration or application API and that another contextual spawn cannot accidentally receive them.
4.4 Binding behavior
swactor.run(main) MUST:
- locate the private bootstrap descriptor;
- perform one versioned bootstrap claim;
- receive and validate the arena descriptor and private session material;
- establish child routing;
- attach the child data plane;
- notify the host that context attachment succeeded;
- invoke
main(ctx)using the existing PythonContextand data-plane API.
A missing, closed, malformed, incompatible, already-claimed, or rejected
bootstrap MUST raise BootstrapError. Attachment errors MUST retain their
existing typed translation where possible and MUST also resolve the host-side
bootstrap as failed.
swactor.run MUST NOT silently fall back to an uncontextualized execution.
5. Lifecycle contract
Native process facts and context attachment facts are related but distinct.
5.1 Observable events
The contextual layer exposes:
Process(ProcessOutput)
ContextReady
BootstrapFailed { reason }
ProcessOutput remains owned by swactor-process and retains its current
Started, SpawnFailed, Exited, Error, stdout, and stderr semantics.
5.2 Ordering and resolution
ContextReadyMUST occur only afterProcess(Started).ContextReadyandBootstrapFailedare mutually exclusive.- At most one context-resolution event may be emitted.
- If OS spawn fails, the contextual layer emits the native
SpawnFailedfact and no context-resolution event: no child existed to claim a context. - After
Process(Started), a process terminal event MUST be preceded by exactly one context-resolution event. - Exit, channel closure, attachment failure, or attachment deadline before
readiness resolves as
BootstrapFailed. BootstrapFailedafter OS start MUST request native process termination.- A late claim, attachment result, timeout, or stop acknowledgement MUST NOT change a resolved context outcome.
- Native process stdout and stderr remain observable before and after context resolution until the native process terminates.
5.3 Readiness
OS Started means only that the executable was created. ContextReady means the
binding has attached and its data plane is usable. Callers MUST use
ContextReady, not Started, when they require Swactor API availability.
The attachment deadline begins after OS start. It is configured by the contextual spawner and driven by its clock service; bindings MUST NOT embed a separate hard-coded policy deadline.
5.4 Stop and cleanup
A stop request may race provisioning, OS spawn, bootstrap claim, attachment, or exit. It MUST be idempotent.
- Before OS start, stop prevents or cancels spawn where possible.
- After OS start, stop delegates to
send_process_commandand its existing terminate/kill policy. - Stop before readiness resolves the context as failed unless OS spawn itself fails first.
- Bootstrap channel closure and session revocation may begin immediately after context failure.
- Arena backing and resources reachable by the child MUST remain owned until the OS child has terminated.
- Cleanup effects MUST execute at most once and MUST eventually complete after the process reaches a terminal state.
- No terminal path may leave a routable child session, active capability, bootstrap descriptor, or arena owner behind.
6. Behavioral invariants
The implementation and every test oracle MUST enforce these invariants.
Identity and isolation
- I1: One contextual spawn owns one execution identity, session capability, host session, arena, and bootstrap channel.
- I2: A bootstrap handle can be claimed at most once.
- I3: A handle created for execution A cannot attach execution B.
- I4: Concurrent spawn cannot leak one Swactor-owned child descriptor into another child.
- I5: Stale events from an earlier execution or generation cannot affect a later execution.
Ordering and outcomes
- I6: Context cannot resolve before OS spawn resolves.
- I7: Context ready requires OS start, accepted claim, successful routing, and successful data-plane attachment.
- I8: Context ready and bootstrap failed cannot both occur.
- I9: Once emitted, process and context terminal facts are immutable.
- I10: Started contextual processes produce one context resolution before their native terminal event is forwarded.
- I11: Native spawn failure never masquerades as bootstrap failure.
Authorization and API boundary
- I12: The session accepts only its own capability and current generation.
- I13: Session access limits every namespace open independently of path discoverability.
- I14: No child-facing environment or argument exposes Swactor routing, actor, capability, endpoint, or arena internals.
- I15: Native spawn receives no contextual authority or resources.
Ownership and cleanup
- I16: Bootstrap resources outlive the attempt to
execthe intended child. - I17: Context resources outlive the running child and no longer.
- I18: Revocation and cleanup are idempotent under duplicate and reordered completion events.
- I19: Quiescence after any terminal path leaves no live bootstrap endpoint, host session, session capability, route, arena owner, or deadline.
- I20: Failure in one execution cannot stop, revoke, or corrupt another.
7. Public API target
Exact Rust layout may adapt to existing actor conventions, but the ownership and observable types below are normative.
7.1 Retained process API
ProcessSpec
ProcessCommand
ProcessOutput
ProcessOutputConfig
spawn_local_process
send_process_command
7.2 Process-core resource seam
swactor-process adds a narrow resource-bearing spawn primitive:
pub struct ProcessSpawnResources {
// Owned descriptor mappings; construction validates unique child targets.
}
pub fn spawn_local_process_with_resources(
ctx: &Ctx,
sender: &ExternalSender,
spec: ProcessSpec,
resources: ProcessSpawnResources,
output: ProcessOutputConfig,
) -> Result<ActorAddress, Error>;
ProcessSpawnResources owns descriptors until the OS spawn attempt resolves.
Descriptor sources remain close-on-exec in the parent. Child descriptor mapping
MUST happen atomically in the child through posix_spawn file actions or an
equivalent safe pre_exec mapping; the implementation MUST NOT create an
ambient parent-side non-CLOEXEC inheritance window.
The existing spawn_local_process is the empty-resource path.
7.3 Contextual process API
A new swactor-process-context composition crate exposes:
pub struct ContextualProcessSpawner { /* node services */ }
pub struct ContextualProcessSpec {
pub process: ProcessSpec,
pub access: SessionAccess,
pub attach_deadline: Duration,
}
pub enum ContextualProcessOutput {
Process(ProcessOutput),
ContextReady,
BootstrapFailed { reason: BootstrapFailure },
}
impl ContextualProcessSpawner {
pub fn spawn(
&self,
ctx: &Ctx,
sender: &ExternalSender,
spec: ContextualProcessSpec,
output: ContextualProcessOutputConfig,
) -> Result<ActorAddress, Error>;
}
ContextualProcessSpawner is constructed once from node-owned runtime,
transport, namespace, arena, and route services. Per-spawn callers cannot supply
raw host actor addresses, endpoint addresses, arena generations, or session
capabilities.
7.4 Data-plane terminology
Clean cutover:
JobCapability → SessionCapability
JobContext → SessionAccess
run_id → execution_id
JobHandoff → removed
install_session_env → removed
No compatibility aliases or deprecated environment path remain.
8. Code architecture
8.1 Dependency direction
swactor-process data-plane
\ /
\ /
swactor-process-context
|
language bindings
swactor-process MUST NOT depend on the data plane or contextual-spawn crate.
The contextual crate may depend on both. Python consumes the guest bootstrap
helper and existing data-plane API.
8.2 Deterministic coordinator
The contextual crate MUST separate lifecycle decisions from side effects.
A small coordinator owns plain state and implements:
apply(Event) -> ordered list of Effect
Representative input events:
SpawnRequested
ProvisionSucceeded / ProvisionFailed
ProcessStarted / ProcessSpawnFailed / ProcessExited / ProcessError
BootstrapClaimed / BootstrapRejected / BootstrapClosed
AttachmentSucceeded / AttachmentFailed
AttachmentDeadline
StopRequested
SessionFault
CleanupCompleted
Representative effects:
ProvisionSession
SpawnNativeProcess
ArmAttachmentDeadline / CancelAttachmentDeadline
AcceptBootstrap / RejectBootstrap / CloseBootstrap
EmitContextReady / EmitBootstrapFailed / EmitProcessOutput
StopNativeProcess
RevokeSession
ReleaseArena
Finish
The actor adapter executes effects through narrow ports and feeds their outcomes back as events. It MUST NOT contain independent lifecycle policy. The pure coordinator is production code, not a test-only copy.
Required ports are limited to:
- process spawn/control;
- session and arena provisioning;
- bootstrap transport;
- clock/deadline scheduling;
- lifecycle output.
This split exists to make races, failures, and cleanup exhaustively testable without real time, OS scheduling, or network nondeterminism. It MUST NOT grow into a generic workflow or job framework.
8.3 Bootstrap implementation
The bootstrap transport belongs beside the data-plane bootstrap contract. Its host and guest helpers own framing, version negotiation, descriptor transfer, and closure. Language bindings MUST call the guest helper rather than parse wire fields.
The arena header and mapping validation remain data-plane concerns. Actor routing and session authorization remain private bootstrap payload fields.
9. Deterministic contract test architecture
9.1 Test objective
Tests MUST prove behavior over arbitrary partial orders of legal lifecycle events, not only hand-authored happy paths. They MUST check invariants after each step and at quiescence. They MUST also prove that the checker rejects traces just outside the legal contract.
Tests MUST NOT depend on sleeps, wall-clock timing, random actor scheduling, real network timing, or inspection of private implementation fields.
9.2 Scenario DAG
The contextual crate provides a test harness with a typed ScenarioDag:
ScenarioDag
nodes: typed external actions or port completions
edges: required happens-before relationships
identities: execution/session/generation correlation
faults: explicit selected failure outcomes
The generator MUST:
- choose one or more concurrent executions;
- select legal terminal outcomes for provisioning, spawn, claim, attachment, stop, and exit;
- add mandatory causal edges, such as spawn before OS start and claim before attachment result;
- add arbitrary acyclic ordering edges between otherwise concurrent actions;
- include races such as stop versus start, timeout versus claim, exit versus attachment, duplicate delivery, and stale completion;
- reject contradictory outcome sets rather than normalizing them silently;
- shrink while preserving graph validity and mandatory causal edges.
For each generated DAG, the harness executes multiple topological linearizations. Small DAGs SHOULD execute every topological order; larger DAGs execute deterministic seeded linearizations emphasizing first/last placement of concurrent boundary events.
9.3 Deterministic driver
The SUT uses the production coordinator with fake ports:
- virtual monotonic clock;
- deterministic execution/session/generation identifiers;
- synthetic owned-descriptor identities;
- recorded process, bootstrap, session, and output effects;
- explicit effect completion controlled by DAG nodes.
After every delivered event, the harness records:
input event
emitted effects
public lifecycle outputs
resource ledger
pending deadlines
context resolution
process resolution
No fake may make lifecycle decisions on behalf of the coordinator.
9.4 Independent contract oracle
The oracle MUST be declarative and separate from the coordinator transition implementation. It checks the trace and resource ledger against §5 and §6; it MUST NOT call the coordinator to calculate expected behavior.
At each prefix it checks safety properties, including uniqueness, ordering, isolation, authorization, and terminal monotonicity. At quiescence it also checks liveness obligations: required resolution occurred and the resource ledger is empty.
Generated traces and minimal regressions are persisted using the repository's
existing proptest regression mechanism.
9.5 Deliberate rejection tests
The suite MUST bind each contract from both sides:
- generate or construct a legal DAG and prove every selected linearization is accepted;
- make one minimal illegal mutation and prove the oracle rejects it with the expected invariant identifier.
Required mutation pairs include:
| Accepted boundary | Deliberately rejected neighbor |
|---|---|
| one claim | duplicate claim (I2) |
| execution A claims A's handle | execution B claims A's handle (I3) |
| child inherits its own descriptor | child inherits a sibling descriptor (I4) |
| ready after start and attachment | ready before start or attachment (I6, I7) |
| one context outcome | ready and failed both emitted (I8) |
| bootstrap failure after start | bootstrap failure used for OS spawn failure (I11) |
| authorized path open | open outside session prefixes (I13) |
| native spawn with no context | native spawn receives bootstrap authority (I15) |
| cleanup once after terminal | cleanup omitted or repeated non-idempotently (I18, I19) |
| stale event ignored | stale event changes current execution (I5) |
| one execution fails in isolation | sibling resources are revoked (I20) |
These are passing tests that deliberately feed invalid traces to the checker and assert a specific rejection. The suite MUST also include checker-calibration fixtures with deliberately broken effect ledgers. This prevents a vacuous oracle that accepts everything or never observes cleanup leaks.
9.6 Boundary integration tests
Model tests do not replace real boundary verification:
- Process descriptor tests: real concurrent Linux children prove fixed-target
inheritance,
CLOEXEC, ownership throughexec, and no sibling leakage. - Bootstrap tests: real Unix sockets prove one-use claim, version rejection,
truncated framing, peer closure, and
SCM_RIGHTSarena transfer. - Data-plane tests: real session attachment proves capability and generation rejection and cleanup.
- Python test: a real Python child calls
swactor.run, observes usablectx.data, performs one namespace operation, and exits successfully. - Failure Python tests: missing claim, duplicate claim, attachment rejection, and user exception remain distinguishable.
- Native regression tests: existing
swactor-processlifecycle, output, and stop contracts remain unchanged.
Integration tests use explicit synchronization events or bounded virtual/test engine progress. They MUST NOT use sleep as correctness synchronization.
10. Migration map
| Current | Target | Action |
|---|---|---|
swactor-process::spawn_local_process |
Native process primitive | Retain unchanged behavior. |
ProcessSpec |
OS execution description | Keep free of context internals. |
| No owned inherited-resource seam | ProcessSpawnResources |
Add and verify atomic child-only inheritance. |
crates/process/pipeline.rs and yaml.rs |
Nothing | Remove job/pipeline layer and exports, subject to final callsite inventory. |
swactor-job-runner |
Nothing | Remove crate, FSM, wire protocol, packaging, and tests. |
| Myelin job deployment/reconciliation | Contextual execution submission | Replace required node launch behavior; delete setup/workspace/output job paths. |
JobHandoff and environment assembly |
Private bootstrap host endpoint | Replace; no compatibility path. |
| Four bootstrap environment variables | Fixed opaque bootstrap descriptor | Remove constants, parsing, tests, and deployment assumptions. |
JobCapability |
SessionCapability |
Rename every wire and API use. |
JobContext { run_id, ... } |
SessionAccess { execution_id, ... } |
Rename and migrate serialized/configured uses. |
install_session_env |
Nothing | Delete. |
Python job.rs |
Context/bootstrap implementation | Rename module and internal job-named symbols. |
JobRouting |
Private context routing state | Rename; never expose to guest code. |
swactor.run(main) |
Same guest API | Preserve observable behavior while replacing bootstrap source. |
Dashboard job-runner-* fixtures |
Execution/process labels | Update fixtures without adding dashboard control behavior. |
| Draft job-runner spec | Superseded | Archive or remove when this migration lands. |
No deprecated aliases, old environment fallback, dual bootstrap protocol, or job-runner compatibility shim may remain after migration.
11. Implementation sequence
Each stage must leave one authoritative path for the contract it introduces. Temporary compatibility is allowed only within an unmerged implementation branch and must not appear in the completed feature.
- Inventory: confirm all process pipeline, job-runner, Myelin, deployment, dashboard fixture, Python, and data-plane callsites named in §10.
- Process resources: implement owned child descriptor mapping and its real Linux isolation tests.
- Terminology cutover: rename data-plane job capability/context concepts to session concepts across wire codecs, tests, and bindings.
- Bootstrap channel: implement host/guest one-use protocol and boundary tests; keep private payload construction in Swactor.
- Deterministic coordinator: implement event/effect core, ports, contract oracle, DAG generator, legal properties, and deliberate rejection fixtures.
- Contextual actor: connect coordinator effects to process, data-plane, bootstrap, clock, and output adapters.
- Python migration: make
swactor.runclaim the bootstrap channel; remove environment parsing and hard-coded attachment deadline. - End-to-end proof: spawn a real Python process through the contextual API
and exercise
ctx.data. - Application migration: replace the node-side job execution path with contextual process submission where required for current application behavior.
- Deletion: remove job-runner crate, process pipeline/YAML job layer, old codecs, app job FSMs, workspace/output packaging, environment bootstrap, and obsolete tests/configuration.
- Final verification: run focused suites, workspace compilation, and static absence checks for all removed symbols and environment variables.
Deletion follows successful migration of required callers; it is not deferred as follow-up cleanup.
12. Acceptance criteria
The feature is complete only when all of the following are true:
- Native process behavior remains compatible and context-free.
- A contextual Python process reaches
ContextReady, usesctx.data, and exits zero through real process supervision. - The child receives one opaque bootstrap descriptor and no Swactor bootstrap environment contract.
- Concurrent contextual children cannot claim or inherit each other's resources.
- Every started contextual process emits exactly one context resolution before its native terminal event.
- Every failure and stop race reaches quiescence with an empty contextual resource ledger.
- Arbitrary generated legal event DAGs satisfy every invariant under tested topological linearizations.
- Minimal illegal mutations are rejected with the intended invariant IDs, and checker-calibration fixtures detect deliberately broken ledgers.
- Python guest APIs and existing data-plane operations remain usable without caller-supplied routing or capability internals.
swactor-job-runner, process job/pipeline APIs, job-named data-plane session types, leaked bootstrap variables, and all compatibility paths are absent.- UI submission, remote transfer, generalized interception, scheduling, and provisioning remain outside this feature.