Skip to content

openjd-sessions: brittle-session contract not enforced — ending_only never set on action failure, session can return to Ready #287

Description

@crowecawcaw

Summary

The "brittle-session" contract is not enforced by the persistent state it relies on. After an action completes Failed/Canceled/Timeout, a Session is supposed to become permanently ending-only (only exit_environment/cleanup allowed, never returning to Ready). But the ending_only flag that is meant to enforce this is never set on any action-failure path, so a failed action followed by exit_environment(keep_session_running = true) (whose onExit succeeds, or which has no onExit) recomputes the session back to Ready and wrongly accepts new run_task / enter_environment calls.

Confirmed against main @ 99871a0293ef0ebfb791cdef6d38c639914463db. Crate: openjd-sessions (src/session.rs).

Expected vs. actual

specs/sessions/session.md § Brittle Sessions states the ending_only flag is set when "an action completes with ActionState::Failed, Canceled, or Timeout." In the code, self.ending_only = true is assigned in exactly one place — exit_environment when !keep_session_running (src/session.rs:1387). No action-finalize path sets it; they set only the transient self.state = SessionState::ReadyEnding.

The post-action state is then computed from the flag:

  • normal drive_action finalize (~session.rs:2060): state = if self.ending_only || final_state != Success { ReadyEnding } else { Ready }
  • no-onExit-script exit path (~session.rs:1562): state = if self.ending_only { ReadyEnding } else { Ready }

New actions are gated on state only (enter_environment, run_task, run_subprocess), never on ending_only.

Reproduction (public API)

  1. Enter an environment whose onExit succeeds (or has none).
  2. run_task(...) that failsfinal_state = Failedstate = ReadyEnding, but ending_only stays false.
  3. exit_environment(id, keep_session_running = true)ending_only not set; onExit succeeds → final_state = Success → state recomputed to Ready.
  4. A subsequent run_task / enter_environment is now accepted — the session has "recovered" from a state it should never leave.

Final state is Ready (bug); the contract requires ReadyEnding.

Suggested fix

Set self.ending_only = true on every action-finalize path that yields a non-Success terminal state (the normal drive_action finalize, the drive_action error path, fail_action_setup, and run_subprocess_via_helper), so the flag reflects the contract regardless of the later exit's keep_session_running value.

Reachability

The bundled openjd CLI does call exit_environment(..., keep_session_running = true, ...) during teardown (crates/openjd-cli/src/run/), but does not dispatch a new action afterward, so the CLI binary itself does not visibly misbehave. The defect is a violation of the Session public-API state-machine contract and is observable by any library consumer (e.g. a worker agent) that keeps a session running after a failed action.

How it was found

Surfaced during a formal-model audit of the sessions runtime: the model's state-machine spec requires the session to stay ending-only after any failure, and comparing that invariant against the code showed ending_only is never set on the failure paths. Verified by two independent adversarial code reviews against the sha above.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions