swactor/docs/specs/drafts/SWACTOR_CONTEXTUAL_PROCESS_SPEC.md

681 lines
25 KiB
Markdown
Raw Normal View History

# Swactor Contextual Process Feature and Migration Specification
Status: Implemented
Last modified: c705c7428960d287f190cad6bfbf57c193da31df
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:
1. **Native process:** the existing managed OS process, with lifecycle,
termination, stdout, and stderr supervision.
2. **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:
```python
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:
1. validate its process specification and session access;
2. provision the arena, session capability, host session, and private bootstrap
channel;
3. transfer the child end of that channel through the process core as an
explicitly owned inherited resource;
4. start the OS child using the existing process actor;
5. allow the installed binding to claim the bootstrap exactly once;
6. construct a usable binding context without caller coordination;
7. resolve context readiness or bootstrap failure;
8. preserve native process lifecycle facts;
9. stop a started child whose context cannot become ready;
10. 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:
```text
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:
1. locate the private bootstrap descriptor;
2. perform one versioned bootstrap claim;
3. receive and validate the arena descriptor and private session material;
4. establish child routing;
5. attach the child data plane;
6. notify the host that context attachment succeeded;
7. invoke `main(ctx)` using the existing Python `Context` and 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:
```text
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
- `ContextReady` MUST occur only after `Process(Started)`.
- `ContextReady` and `BootstrapFailed` are mutually exclusive.
- At most one context-resolution event may be emitted.
- If OS spawn fails, the contextual layer emits the native `SpawnFailed` fact 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`.
- `BootstrapFailed` after 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_command` and 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 `exec` the 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
```text
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:
```rust
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:
```rust
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:
```text
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
```text
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:
```text
apply(Event) -> ordered list of Effect
```
Representative input events:
```text
SpawnRequested
ProvisionSucceeded / ProvisionFailed
ProcessStarted / ProcessSpawnFailed / ProcessExited / ProcessError
BootstrapClaimed / BootstrapRejected / BootstrapClosed
AttachmentSucceeded / AttachmentFailed
AttachmentDeadline
StopRequested
SessionFault
CleanupCompleted
```
Representative effects:
```text
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`:
```text
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:
1. choose one or more concurrent executions;
2. select legal terminal outcomes for provisioning, spawn, claim, attachment,
stop, and exit;
3. add mandatory causal edges, such as spawn before OS start and claim before
attachment result;
4. add arbitrary acyclic ordering edges between otherwise concurrent actions;
5. include races such as stop versus start, timeout versus claim, exit versus
attachment, duplicate delivery, and stale completion;
6. reject contradictory outcome sets rather than normalizing them silently;
7. 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:
```text
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:
1. generate or construct a legal DAG and prove every selected linearization is
accepted;
2. 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 through `exec`, and no sibling leakage.
- **Bootstrap tests:** real Unix sockets prove one-use claim, version rejection,
truncated framing, peer closure, and `SCM_RIGHTS` arena transfer.
- **Data-plane tests:** real session attachment proves capability and generation
rejection and cleanup.
- **Python test:** a real Python child calls `swactor.run`, observes usable
`ctx.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-process` lifecycle, 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.
1. **Inventory:** confirm all process pipeline, job-runner, Myelin, deployment,
dashboard fixture, Python, and data-plane callsites named in §10.
2. **Process resources:** implement owned child descriptor mapping and its real
Linux isolation tests.
3. **Terminology cutover:** rename data-plane job capability/context concepts to
session concepts across wire codecs, tests, and bindings.
4. **Bootstrap channel:** implement host/guest one-use protocol and boundary
tests; keep private payload construction in Swactor.
5. **Deterministic coordinator:** implement event/effect core, ports, contract
oracle, DAG generator, legal properties, and deliberate rejection fixtures.
6. **Contextual actor:** connect coordinator effects to process, data-plane,
bootstrap, clock, and output adapters.
7. **Python migration:** make `swactor.run` claim the bootstrap channel; remove
environment parsing and hard-coded attachment deadline.
8. **End-to-end proof:** spawn a real Python process through the contextual API
and exercise `ctx.data`.
9. **Application migration:** replace the node-side job execution path with
contextual process submission where required for current application
behavior.
10. **Deletion:** remove job-runner crate, process pipeline/YAML job layer, old
codecs, app job FSMs, workspace/output packaging, environment bootstrap, and
obsolete tests/configuration.
11. **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:
1. Native process behavior remains compatible and context-free.
2. A contextual Python process reaches `ContextReady`, uses `ctx.data`, and exits
zero through real process supervision.
3. The child receives one opaque bootstrap descriptor and no Swactor bootstrap
environment contract.
4. Concurrent contextual children cannot claim or inherit each other's
resources.
5. Every started contextual process emits exactly one context resolution before
its native terminal event.
6. Every failure and stop race reaches quiescence with an empty contextual
resource ledger.
7. Arbitrary generated legal event DAGs satisfy every invariant under tested
topological linearizations.
8. Minimal illegal mutations are rejected with the intended invariant IDs, and
checker-calibration fixtures detect deliberately broken ledgers.
9. Python guest APIs and existing data-plane operations remain usable without
caller-supplied routing or capability internals.
10. `swactor-job-runner`, process job/pipeline APIs, job-named data-plane session
types, leaked bootstrap variables, and all compatibility paths are absent.
11. UI submission, remote transfer, generalized interception, scheduling, and
provisioning remain outside this feature.