HarperFast / HarperFast/harper

Determinism contract for workflow code (SES Compartment policy)

Open
#756 1 comment 0 reactions 0 assignees View on GitHub
enhancement
Dominant language
JavaScript
Stars
89
Forks
10
Avg merge
2d 2h
Merged PRs (30d)
205

Description

Part of #752 (Durable Execution v0.1 epic).

## Problem

Replay safety requires the deterministic portion of a workflow function to produce the same control flow on every replay. If a workflow calls \`Date.now()\` directly, or reads \`Math.random()\`, or makes an unfettered \`fetch\`, replay produces different control flow and the engine cannot reason about which steps to skip.

Engines in this category (Temporal, DBOS, Restate) settle on mechanical enforcement of determinism rather than convention + warnings — the failure mode of accidental non-determinism is too easy to hit and too hard to debug after the fact.

## Proposed shape

Workflow \`run\` functions execute inside an **SES Compartment** with non-deterministic globals shadowed or blocked:

| Capability | Behavior in workflow context |
|---|---|
| \`Date.now\`, \`Date()\` | Returns the *deterministic* clock — the timestamp recorded in the engine's replay log, not wall clock |
| \`Math.random\` | Returns from a deterministic PRNG seeded per workflow / step |
| \`setTimeout\`, \`setInterval\` | Replaced by \`ctx.sleep\` — direct use throws at runtime |
| Free \`fetch\` / \`http\` / \`net\` / \`fs\` | Blocked — must go through \`ctx.step\` |
| \`crypto.randomUUID\`, \`crypto.getRandomValues\` | Shadowed with a deterministic source seeded per step |
| Globals (\`globalThis.*\`) | Frozen per Compartment policy |

The only way to get external data or side effects is through \`ctx.step\` (or \`ctx.atomic\`), which intercepts the call, checkpoints the result, and returns the cached value on replay.

## Existing substrate

SES Compartment support is already wired up in [\`security/jsLoader.ts\`](https://github.com/HarperFast/harper/blob/main/security/jsLoader.ts#L87). This issue is the **policy layer** on top of it — the specific endowments, the shimmed clock/PRNG, the runtime interception of forbidden calls, and the developer-error surface.

## Developer error surface

Mechanical enforcement is brittle if the error message is opaque. Concrete asks:

- Calling \`Date.now()\` should not silently return the deterministic clock — it should *use* it, but the runtime should also log a one-time warning to a developer-visible diagnostic stream so accidental non-determinism is caught in development.
- Calling \`fetch\` directly should throw a typed \`WorkflowDeterminismError\` with a message pointing at the surrounding step and the suggested wrap (\"wrap in \`ctx.step('fetch-foo', () => fetch(...))\`\").
- Replay-time mismatch (a step's recorded input hash != current input hash) should produce a distinct \`WorkflowReplayMismatchError\` with the diff.

## Where it runs

Most of this is developable against the existing SES integration without core changes. The exception is the step-interception fast path: at hot-loop scale, the \`ctx.step\` runtime check (replay vs. execute, idempotency-key generation, checkpoint commit) benefits from being in the workflow runtime directly rather than in userland JS. Calling that out as the one core-adjacent piece of this issue.

## Open questions

- Mechanical enforcement or convention-plus-warnings? Proposal recommends mechanical; this issue assumes that — but it's worth a real conversation with whoever ends up owning the developer experience.
- Should the deterministic clock be the engine's logical clock (monotonic per workflow) or the wall-clock-at-replay-source-time (the original timestamp)? Different engines pick differently; Temporal uses replay-source wall time.
- Per-step or per-workflow PRNG seeding? Per-step is safer but slightly noisier.

---

🤖 Filed by [Claude](https://claude.com/claude-code) on behalf of @kriszyp

Contributor guide

Open the contributing guide

Research direction

Start by reading security/jsLoader.ts around the existing SES Compartment integration. Define the policy layer there for deterministic globals, blocked capabilities, step interception, and the specified developer errors; done requires a coherent enforcement policy and error surface, while the clock, PRNG, and mechanical-versus-warning choices remain open questions.

Written by the indexing model from the issue text.

Assessment

Tech stack
javascript, node.js
Domain
backend, distributed-systems, security
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.