Skip to content

FE-1220: Harden and document Petrinaut optimization execution isolation#9058

Draft
kube wants to merge 3 commits into
mainfrom
cf/fe-1220-petrinaut-optimization-execution-isolation
Draft

FE-1220: Harden and document Petrinaut optimization execution isolation#9058
kube wants to merge 3 commits into
mainfrom
cf/fe-1220-petrinaut-optimization-execution-isolation

Conversation

@kube

@kube kube commented Jul 19, 2026

Copy link
Copy Markdown
Collaborator

🌟 What is the purpose of this PR?

Hardens and documents the isolation of the Petrinaut optimization execution path (NodeAPI → optimizer → CLI). Threat-models the path, terminates the complete CLI process group on every shutdown, bounds CLI CPU / memory / process count, adds a study wall-clock ceiling, and reaps orphans — with the deployment isolation model and remaining risks written down.

🔗 Related links

  • FE-1220 — this issue
  • FE-1217 — parent hardening issue
  • SRE-830 — out-of-repo ECS task settings (read-only rootfs, memory sizing, deny-all egress, pidsLimit)

🚫 Blocked by

🔍 What does this change?

  • petrinaut_client.py: a SIGKILL sweep of the CLI process group on every close path, so descendants cannot outlive the study; resource.prlimit (Linux) bounds each CLI process — RLIMIT_CPU / RLIMIT_AS / RLIMIT_NPROC, configurable via HASH_PETRINAUT_OPT_CLI_* (0 disables).
  • petrinaut_optimizer.py: a study wall-clock ceiling (HASH_PETRINAUT_OPT_MAX_STUDY_SECONDS, default 900) that terminates the stream and the CLI.
  • Dockerfile: tini as PID 1 to reap any orphan that escapes the group sweep.
  • docs/threat-model.md (new): the execution path, per-layer mitigations, required out-of-repo deployment configuration, and the documented decision that container/process isolation is sufficient (no additional V8 sandbox required). README and OPTIMIZATION_INTEGRATION.md document the new env vars and the cancellation/shutdown contract.

Pre-Merge Checklist 🚀

🚢 Has this modified a publishable library?

This PR:

  • does not modify any publishable blocks or libraries, or modifications do not need publishing

📜 Does this require a change to the docs?

The changes in this PR:

  • require changes to docs which are made as part of this PR

🕸️ Does this require a change to the Turbo Graph?

The changes in this PR:

  • do not affect the execution graph

⚠️ Known issues

  • RLIMIT_NPROC is enforced by Linux against the service user's total task count, not this child's own descendants; container-level pidsLimit (SRE-830) is the backstop.
  • prlimit is applied from the parent immediately after spawn, so the child is briefly unbounded; container limits are the outer backstop.
  • Task-level settings (read-only rootfs, memory limits, egress policy, pidsLimit) live in the out-of-repo infra and are tracked in SRE-830.

🛡 What tests cover this?

New apps/petrinaut-opt unit tests: real-subprocess grandchild process-group sweep (mutation-verified), resource-limit application and env parsing, and study wall-clock-ceiling termination.

❓ How to test this?

  1. cd apps/petrinaut-opt && uv run pytest
  2. Confirm the process-group-sweep, resource-limit, and study-ceiling tests pass (Linux exercises prlimit; it is skipped on macOS).

kube added 2 commits July 19, 2026 03:41
…lation latency

NodeAPI (apps/hash-api):
- Race the backpressure wait against drain, close, and the request
  lifecycle abort signal, removing the losing listeners via an internal
  AbortController so stalled writes cannot leak listeners, strand a
  rejected promise, or hold an optimization slot past the timeouts.
- Mark terminal events as committed at write hand-off so an abort during
  the final backpressured write cannot append a second, contradictory
  terminal event.
- Write the failure-path terminal event without awaiting backpressure so
  teardown never blocks on a stalled client.
- Skip transport heartbeats while the response buffer needs draining.

Petrinaut Optimizer (apps/petrinaut-opt):
- PetrinautModel.close(graceful=False) signals the CLI process group
  immediately instead of waiting up to five seconds for stdin EOF; the
  graceful EOF wait is now reserved for studies that completed normally.
  All failure, timeout, and cancellation paths terminate promptly.
- Share one idempotent per-run cleanup between the stream wrapper and a
  StreamingResponse background task, so a client that aborts before the
  body is ever pulled cannot leak the admission slot or a live CLI.
- Close an abandoned CLI via an initializer done-callback when a second
  cancellation races the init-cancel recovery.
- Emit Retry-After on the 429 study-limit response and declare it in the
  OpenAPI spec.

Tests cover drain-first, close-first, client abort, idle timeout during
backpressure, single-terminal-event, heartbeat suppression, slot release,
prompt process-group termination, and 429/Retry-After propagation across
NodeAPI, the optimizer client, and the Python service.
Petrinaut Optimizer (apps/petrinaut-opt):
- Apply resource.prlimit bounds (Linux) to every spawned CLI process:
  RLIMIT_CPU, RLIMIT_AS, and RLIMIT_NPROC, each configurable via
  HASH_PETRINAUT_OPT_CLI_* environment variables with zero disabling.
- Sweep the CLI's process group with SIGKILL at the end of every close
  path, so descendants that remain in the group cannot outlive the study
  even when the CLI itself exits gracefully on stdin EOF.
- Enforce a per-study wall-clock ceiling
  (HASH_PETRINAUT_OPT_MAX_STUDY_SECONDS, default 900 s) in both stream
  loops, emitting a terminal error frame and closing the CLI promptly;
  a study whose terminal sentinel is already queued when the deadline
  lapses is still reported as completed.
- Run tini as PID 1 in the Docker image so orphaned descendants are
  reaped instead of accumulating as zombies.

Documentation:
- New apps/petrinaut-opt/docs/threat-model.md: execution-path threat
  model, per-layer mitigations, known limitations (per-UID RLIMIT_NPROC
  accounting, setsid escape from the group sweep, prlimit timing, pgid
  recycling), required out-of-repo deployment configuration (read-only
  rootfs, task memory sized against RLIMIT_AS, deny-all egress,
  pidsLimit), and the decision that no additional V8 sandbox is needed.
- README documents the new environment variables; the CLI's
  OPTIMIZATION_INTEGRATION.md documents the cancellation and shutdown
  contract.

Tests cover the grandchild sweep (real subprocess, mutation-verified),
prompt-vs-graceful shutdown ordering, rlimit application and parsing,
ceiling-based termination, and the completed-at-the-boundary case. Real
subprocess tests pin the limits off so per-UID NPROC accounting on busy
hosts cannot fail them.
@kube kube self-assigned this Jul 19, 2026
@vercel

vercel Bot commented Jul 19, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

3 Skipped Deployments
Project Deployment Actions Updated (UTC)
hash Ignored Ignored Preview Jul 19, 2026 2:54am
hashdotdesign-tokens Ignored Ignored Preview Jul 19, 2026 2:54am
petrinaut Skipped Skipped Jul 19, 2026 2:54am

@github-actions github-actions Bot added area/infra Relates to version control, CI, CD or IaC (area) area/libs Relates to first-party libraries/crates/packages (area) type/eng > frontend Owned by the @frontend team area/tests New or updated tests area/apps labels Jul 19, 2026
@vercel
vercel Bot temporarily deployed to Preview – petrinaut July 19, 2026 02:53 Inactive
@kube
kube marked this pull request as ready for review July 19, 2026 03:14
@kube
kube requested a review from YannisZa July 19, 2026 03:14
@cursor

cursor Bot commented Jul 19, 2026

Copy link
Copy Markdown

PR Summary

Medium Risk
Touches subprocess lifecycle and resource limits on a security-sensitive optimization path; behavior is well-tested but mis-tuned RLIMIT_NPROC or memory limits could affect availability under load.

Overview
Hardens Petrinaut optimization execution isolation and documents the security model for reviewers and operators.

Runtime hardening (petrinaut_client, Dockerfile): every CLI shutdown now ends with a SIGKILL process-group sweep so grandchildren cannot outlive the study; tini is PID 1 to reap orphans. Linux prlimit on each spawned CLI bounds CPU, address space, and process count via HASH_PETRINAUT_OPT_CLI_* env vars. petrinaut_optimizer enforces a per-study wall-clock ceiling (HASH_PETRINAUT_OPT_MAX_STUDY_SECONDS, default 900s) on SSE streams, emitting a terminal error and tearing down the CLI without mislabeling a study that already queued its completion sentinel.

Documentation: new docs/threat-model.md (execution path, mitigations, limitations, required ECS settings, no extra V8 sandbox rationale); README lists isolation env vars; OPTIMIZATION_INTEGRATION.md documents parent cancellation/shutdown expectations.

Tests cover group sweep, rlimit application/parsing, study ceiling vs. boundary completion, and disabling limits in real-subprocess tests.

Reviewed by Cursor Bugbot for commit 47f2fea. Bugbot is set up for automated code reviews on this repo. Configure here.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 47f2fea. Configure here.

f"{json.dumps({'state': 'ERROR', 'message': message})}"
f"\n\n"
)
break

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pending sentinel misclassified as timeout

Medium Severity

When the study wall-clock deadline is reached, timeout handling treats an empty events queue as proof the study is still running. The worker always enqueues the completion sentinel via call_soon_threadsafe, so a study that just finished can look idle until that callback runs, triggering a false execution-limit error instead of the normal done path.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 47f2fea. Configure here.

YannisZa
YannisZa previously approved these changes Jul 19, 2026
@kube
kube marked this pull request as draft July 19, 2026 15:23
@kube

kube commented Jul 19, 2026

Copy link
Copy Markdown
Collaborator Author

Converted back to draft: the stack was reordered so the streaming fixes (#9057) and correlated logging (#9059) can merge first to debug the staging network error (FE-1217). #9059 no longer stacks on this branch; once it merges, this branch needs a rebase over it (reintroducing the sweep/ceiling log events and the threat-model rows that reference logging).

Base automatically changed from cf/fe-1218-petrinaut-optimization-streaming-cancellation to main July 19, 2026 21:00
@kube
kube dismissed YannisZa’s stale review July 19, 2026 21:00

The base branch was changed.

@github-actions github-actions Bot added area/apps > hash* Affects HASH (a `hash-*` app) area/apps > hash-api Affects the HASH API (app) type/eng > backend Owned by the @backend team labels Jul 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/apps > hash* Affects HASH (a `hash-*` app) area/apps > hash-api Affects the HASH API (app) area/apps area/infra Relates to version control, CI, CD or IaC (area) area/libs Relates to first-party libraries/crates/packages (area) area/tests New or updated tests type/eng > backend Owned by the @backend team type/eng > frontend Owned by the @frontend team

Development

Successfully merging this pull request may close these issues.

2 participants