Skip to content

FE-1162: Add Petrinaut optimization workflow#9040

Draft
kube wants to merge 17 commits into
mainfrom
codex/petrinaut-optimization-ui
Draft

FE-1162: Add Petrinaut optimization workflow#9040
kube wants to merge 17 commits into
mainfrom
codex/petrinaut-optimization-ui

Conversation

@kube

@kube kube commented Jul 15, 2026

Copy link
Copy Markdown
Collaborator

🌟 What is the purpose of this PR?

Provide one end-to-end draft for the Petrinaut optimization workflow, from Petrinaut CLI and the Python optimizer service through generated types, NodeAPI, and the HASH/Petrinaut UI.

This consolidates the former four-PR stack into one place so the contract can be iterated on coherently. The next pass will reduce and reshape the apps/petrinaut-opt changes with Yannis; those changes are intentionally still provisional in this draft.

🔗 Related links

  • Yannis's merged Python optimizer API: #8989
  • Petrinaut optimizer Dockerfile: #9031
  • Consolidated intermediate drafts: #9042, #9038, #9037

🔍 What does this change?

Petrinaut CLI and Python service

  • Adds petrinaut serve --model-stdin --stdio, with one immutable model bootstrap followed by JSON-line protocol requests.
  • Runs a selected scenario using its complete flat parameter map and requests one metric by stable ID.
  • Keeps Optuna search-space bounds and maximize / minimize direction in Python rather than the CLI protocol.
  • Includes a typed Python-service reference implementation with bounded requests, lifecycle handling, streamed NDJSON events, OpenAPI generation, and integration documentation.
  • Documents a concrete scenario, optimized versus fixed parameters, metric selection, and Optuna objective in libs/@hashintel/petrinaut-cli/OPTIMIZATION_INTEGRATION.md.

Generated contract and NodeAPI

  • Generates runtime-free TypeScript types from the checked-in FastAPI OpenAPI contract.
  • Adds authenticated Petrinaut optimizer capability, status, and optimization routes to NodeAPI.
  • Validates upstream requests and streamed events at runtime, with timeouts, cancellation, backpressure, concurrency limits, and generic upstream failures.

HASH and Petrinaut UI

  • Adds an authenticated host-to-iframe optimization bridge without exposing service credentials or addresses to the embedded editor.
  • Adds a host-backed optimization provider and a fake website provider for isolated development.
  • Adds a separate capability-gated Optimizations tab.
  • Requires the user to choose a scenario first, keeps parameters fixed by default, permits only a flat parameter search space, and requires exactly one saved metric with a maximize/minimize direction.
  • Streams trial progress and best-so-far results into dedicated views.
  • Adds corresponding Petrinaut user documentation.

🚧 Review status

This remains a draft. The vertical contract and UI are available for joint iteration, but the current Python-service implementation is not proposed as a wholesale replacement for Yannis's implementation. The immediate follow-up is to keep only the CLI-facing and API-contract changes that his service needs, making the resulting diff focused and reviewable by him.

Pre-Merge Checklist 🚀

🚢 Has this modified a publishable library?

This PR:

  • modifies an npm-publishable library and I have added a changeset file(s)

📜 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:

  • affect the execution graph and include the required package-level Turbo configuration

⚠️ Known issues

  • Optimization runs are tied to the browser request; closing or reloading the page cancels the run.
  • The initial contract intentionally supports one objective metric and a flat scenario-parameter search space only.
  • Petrinaut scenarios contain user-authored JavaScript, so production isolation remains required in addition to subprocess restrictions.

🛡 What tests cover this?

  • Petrinaut optimizer Python suite: 38 tests
  • Petrinaut CLI protocol/transport suite: 25 tests
  • NodeAPI optimizer handler: 18 tests
  • HASH host/iframe bridge: 11 tests
  • Focused Petrinaut Optimization UI: 9 tests
  • Petrinaut Core optimization contract: 8 tests
  • Generated-type drift/build and affected TypeScript package checks
  • Real stdio scenario smoke test using metric_profit

❓ How to test this?

  1. Configure HASH_PETRINAUT_OPT_HOST and HASH_PETRINAUT_OPT_PORT for NodeAPI.
  2. Start the Petrinaut optimizer service and HASH.
  3. Open a Petrinaut process containing a scenario with parameters and at least one saved metric.
  4. Go to Simulate → Optimizations, select a scenario, choose flat parameter ranges and one metric/direction, then start the run.
  5. Confirm progressive trials and final best parameters appear, and that cancellation stops the upstream run.

📹 Demo

Not included in this draft.

@vercel

vercel Bot commented Jul 15, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
hash Ready Ready Preview, Comment Jul 17, 2026 11:08am
2 Skipped Deployments
Project Deployment Actions Updated (UTC)
hashdotdesign-tokens Ignored Ignored Preview Jul 17, 2026 11:08am
petrinaut Skipped Skipped Comment Jul 17, 2026 11:08am

@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/apps labels Jul 15, 2026
@kube
kube force-pushed the codex/petrinaut-optimizer-endpoint branch from a6599a8 to d031e87 Compare July 16, 2026 00:06
@kube
kube force-pushed the codex/petrinaut-optimization-ui branch from 0820a43 to 77c2d96 Compare July 16, 2026 00:06
@github-actions github-actions Bot added area/deps Relates to third-party dependencies (area) 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 area/tests New or updated tests labels Jul 16, 2026
@kube kube changed the title FE-1162: Add Petrinaut optimization UI FE-1162: Add Petrinaut optimization workflow Jul 16, 2026
const postToHost = (message: IframeToHostMessage) => {
// The sandboxed iframe has an opaque origin. This still targets only its
// parent window; the host independently verifies `event.source`.
window.parent.postMessage(message, "*");

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Semgrep identified an issue in your code:
The target origin of the window.postMessage() API is set to "*". This could allow for information disclosure due to the possibility of any origin allowed to receive the message.

To resolve this comment:

🔧 No guidance has been designated for this issue. Fix according to your organization's approved methods.

💬 Ignore this finding

Reply with Semgrep commands to ignore this finding.

  • /fp <comment> for false positive
  • /ar <comment> for acceptable risk
  • /other <comment> for all other reasons

Alternatively, triage in Semgrep AppSec Platform to ignore the finding created by wildcard-postmessage-configuration.

You can view more details about this finding in the Semgrep AppSec Platform.

Comment on lines +67 to +145
window.addEventListener("message", (event) => {
if (event.source !== window.parent) {
return;
}

const data = event.data as unknown;
if (
typeof data !== "object" ||
data === null ||
typeof (data as { kind?: unknown }).kind !== "string"
) {
return;
}

const message = data as HostToIframeMessage;
if (
message.kind !== "optimizationResponseStart" &&
message.kind !== "optimizationChunk" &&
message.kind !== "optimizationEnd" &&
message.kind !== "optimizationError"
) {
return;
}

const pending = pendingRequests.get(message.requestId);
if (!pending) {
return;
}

switch (message.kind) {
case "optimizationResponseStart": {
if (pending.responded) {
rejectPendingRequest(
message.requestId,
new Error("The optimizer sent more than one response header"),
);
return;
}
pending.responded = true;
pending.clearResponseStartTimeout();
pending.resolveResponse(
new Response(pending.stream, {
headers: { "content-type": "application/x-ndjson" },
status: message.status,
statusText: message.statusText,
}),
);
break;
}
case "optimizationChunk": {
try {
pending.controller.enqueue(message.bytes);
} catch {
// The consumer may already have cancelled the stream.
}
break;
}
case "optimizationEnd": {
if (!pending.responded) {
rejectPendingRequest(
message.requestId,
new Error("The optimizer ended before sending a response"),
);
return;
}
try {
pending.controller.close();
} catch {
// The stream is already settled.
}
pending.cleanup();
pendingRequests.delete(message.requestId);
break;
}
case "optimizationError":
rejectPendingRequest(message.requestId, new Error(message.message));
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.

Semgrep identified an issue in your code:
No validation of origin is done by the addEventListener API. It may be possible to exploit this flaw to perform Cross Origin attacks such as Cross-Site Scripting(XSS).

To resolve this comment:

🔧 No guidance has been designated for this issue. Fix according to your organization's approved methods.

💬 Ignore this finding

Reply with Semgrep commands to ignore this finding.

  • /fp <comment> for false positive
  • /ar <comment> for acceptable risk
  • /other <comment> for all other reasons

Alternatively, triage in Semgrep AppSec Platform to ignore the finding created by insufficient-postmessage-origin-validation.

You can view more details about this finding in the Semgrep AppSec Platform.

const postToHost = (message: IframeToHostMessage) => {
// The sandboxed iframe has an opaque origin. This still targets only its
// parent window; the host independently verifies `event.source`.
window.parent.postMessage(message, "*");
Comment on lines +67 to +145
window.addEventListener("message", (event) => {
if (event.source !== window.parent) {
return;
}

const data = event.data as unknown;
if (
typeof data !== "object" ||
data === null ||
typeof (data as { kind?: unknown }).kind !== "string"
) {
return;
}

const message = data as HostToIframeMessage;
if (
message.kind !== "optimizationResponseStart" &&
message.kind !== "optimizationChunk" &&
message.kind !== "optimizationEnd" &&
message.kind !== "optimizationError"
) {
return;
}

const pending = pendingRequests.get(message.requestId);
if (!pending) {
return;
}

switch (message.kind) {
case "optimizationResponseStart": {
if (pending.responded) {
rejectPendingRequest(
message.requestId,
new Error("The optimizer sent more than one response header"),
);
return;
}
pending.responded = true;
pending.clearResponseStartTimeout();
pending.resolveResponse(
new Response(pending.stream, {
headers: { "content-type": "application/x-ndjson" },
status: message.status,
statusText: message.statusText,
}),
);
break;
}
case "optimizationChunk": {
try {
pending.controller.enqueue(message.bytes);
} catch {
// The consumer may already have cancelled the stream.
}
break;
}
case "optimizationEnd": {
if (!pending.responded) {
rejectPendingRequest(
message.requestId,
new Error("The optimizer ended before sending a response"),
);
return;
}
try {
pending.controller.close();
} catch {
// The stream is already settled.
}
pending.cleanup();
pendingRequests.delete(message.requestId);
break;
}
case "optimizationError":
rejectPendingRequest(message.requestId, new Error(message.message));
break;
}
});
Comment thread .changeset/fresh-optimizers-stream.md Outdated
Comment on lines +2 to +3
"@hashintel/petrinaut-core": minor
"@hashintel/petrinaut": minor

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Suggested change
"@hashintel/petrinaut-core": minor
"@hashintel/petrinaut": minor
"@hashintel/petrinaut-core": patch
"@hashintel/petrinaut": patch

@kube
kube force-pushed the codex/petrinaut-optimizer-endpoint branch 2 times, most recently from 5a24666 to 5094e3b Compare July 16, 2026 09:46
@kube
kube force-pushed the codex/petrinaut-optimization-ui branch from e246e59 to d868b29 Compare July 16, 2026 09:47
@github-actions github-actions Bot removed the area/tests New or updated tests label Jul 16, 2026
throw new Error("Petrinaut optimizer returned an invalid event");
}
const upstreamEvent: PetrinautOptimizerOptimizationEvent = parsed.data;
if (!response.write(`${JSON.stringify(upstreamEvent)}\n`)) {

@semgrep-code-hashintel semgrep-code-hashintel Bot Jul 16, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Detected directly writing to a Response object from user-defined input. This bypasses any HTML escaping and may expose your application to a Cross-Site-scripting (XSS) vulnerability. Instead, use 'resp.render()' to render safely escaped HTML.

🧁 Fixed in commit 18ce05d 🧁

`Scenario parameter "${identifier}" must be a finite number or boolean`,
);
}
parameterValues[identifier] = parameterValue;
@vercel

vercel Bot commented Jul 16, 2026

Copy link
Copy Markdown

Deployment failed with the following error:

The provided GitHub repository does not contain the requested branch or commit reference. Please ensure the repository is not empty.

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/deps Relates to third-party dependencies (area) 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 type/legal Owned by the @legal team

Development

Successfully merging this pull request may close these issues.

2 participants