Skip to content

Adopt no-ambient-nondeterminism: route time, ids, and randomness through Effect seams - #9

Merged
K-Mistele merged 5 commits into
mainfrom
ambient-nondeterminism-seams
Aug 19, 2026
Merged

Adopt no-ambient-nondeterminism: route time, ids, and randomness through Effect seams#9
K-Mistele merged 5 commits into
mainfrom
ambient-nondeterminism-seams

Conversation

@K-Mistele

Copy link
Copy Markdown
Contributor

What

Adopts automation/no-ambient-nondeterminism (vendored from the TypeOnce catalog) and clears every in-scope finding first, so ambient time and randomness flow through Effect's Clock, Random, Ids, and Crypto seams — mockable in tests instead of reading wall-clock / Math.random / global crypto directly.

The seams

Area Before After
Session timestamps Date.now() in the deletion tombstone + viewed-changes writes Clock.currentTimeMillis (default service)
Session id minting SessionId.create() in prepareSessionLog Ids service; requirement threads to cli.ts, which provides layerLiveIdFactory
Temp-path uniqueness Date.now() / node randomBytes Clock + Random (default services)
OAuth PKCE / state / nonce ambient crypto.getRandomValues / crypto.subtle Effect Crypto service (randomBytes + digest); live NodeCrypto layer already provided via NodeServices

Notes / decisions

  • Behaviour-preserving. Clock/Random are default services (no R change; DeterministicClock already mocks Clock). The Ids requirement threads to the app runtime (cli.ts), matching the existing DeterministicIds test layer. Tests shadow launchSession/prepareSessionLog with the live id layer, so no assertions shift.
  • OAuth security unchanged. NodeCrypto is node:crypto — a real CSPRNG (Effect's general Random is Math.random-backed, so it is not used for tokens). The browser-flow call sites provide NodeCrypto.layer locally, keeping the auth service interfaces at R = never (no consumer/TUI ripple). A test can now provide a deterministic Crypto layer to pin PKCE values.
  • Rule scope. The vendored rule keeps the upstream scope analysis, so a local const crypto = yield* Crypto.Crypto passes while ambient globals are caught. new Date(knownValue) (pure formatting) is not flagged. TUI/theme and test files are excluded via overrides.

Testing

bun run typecheck, bun run lint, bun run format:check all clean. Full test suite passes except fold-codex/CodexLive.vi.test.ts (2 live-network tests, 429 usage_limit_reached, CI-skipped and unrelated).

🤖 Generated with Claude Code

K-Mistele and others added 5 commits August 18, 2026 19:48
Ambient Date.now() and SessionId.create() bypassed the Clock and Ids service
seams the rest of Fold already uses, so session-layout writes and id minting
could not be pinned in tests.

- The SessionLayout deletion tombstone and ViewedChanges now read
  Clock.currentTimeMillis instead of Date.now().
- prepareSessionLog mints via the Ids service; the requirement threads up the
  launch path and the app runtime provides layerLiveIdFactory (cli.ts). The TUI
  acquire sites and the ConfigAgent example provide the same live layer.
- The Launch and SessionLayout tests shadow the launch/prepare helpers with the
  live id layer, preserving behavior while leaving DeterministicIds as a seam.

Behavior-preserving prep for the no-ambient-nondeterminism rule.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
HumanLayer-Session: https://app.dev.codelayer.gg/sessions/01a0179a-5c6f-7ab7-8e1e-92b5a54ffad3
writeConfig's atomic-rename temp path and BashTool's spill filename used
Date.now() and node crypto randomBytes directly. Route them through the Clock
and Random default services so tests can pin the generated names. Runtime
behavior is unchanged - Random defaults to Math.random, which is fine for
filename uniqueness (these are not security tokens).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
HumanLayer-Session: https://app.dev.codelayer.gg/sessions/01a0179a-5c6f-7ab7-8e1e-92b5a54ffad3
PKCE verifier/state/nonce and the S256 challenge digest used the ambient
crypto.getRandomValues / crypto.subtle globals, so their values could not be
pinned in tests. Route them through Effect's Crypto service (randomBytes +
digest); the live NodeCrypto layer is node:crypto - a real CSPRNG - and is
already provided app-wide via NodeServices at cli.ts.

- generatePkce/generateState (codex) and generateXaiPkce + state/nonce (xai)
  now yield Crypto; crypto failures orDie, matching the prior Effect.promise
  defect semantics, so only the R channel changes.
- The browser-flow call sites provide NodeCrypto.layer locally, keeping the
  auth service interfaces at R = never (no consumer or TUI ripple).

Security is unchanged (same node CSPRNG). Tests can now provide a deterministic
Crypto layer to pin PKCE values.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
HumanLayer-Session: https://app.dev.codelayer.gg/sessions/01a0179a-5c6f-7ab7-8e1e-92b5a54ffad3
Vendor the TypeOnce no-ambient-nondeterminism rule into tools/oxlint/automation
(adapted: dropped the .tsx allow-list option, mapped messages to messageIds; the
scope analysis is kept so local bindings like `const crypto = yield* Crypto.Crypto`
pass while the ambient globals are caught) and enforce it as an error.

Ambient Date.now()/new Date()/Math.random() and the global crypto object must go
through Effect's Clock, Random, and Crypto capabilities. TUI/theme and test files
are excluded via overrides. Zero in-scope findings remain after the preceding seam
refactors (session time/ids, temp paths, OAuth CSPRNG).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
HumanLayer-Session: https://app.dev.codelayer.gg/sessions/01a0179a-5c6f-7ab7-8e1e-92b5a54ffad3
Chaining Effect.provide(layerLiveIdFactory) after Effect.provide(NodeServices.layer)
tripped the effect tsgolint multipleEffectProvide check (TS377033), failing
typecheck. Merge both layers with Layer.mergeAll and provide once.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
HumanLayer-Session: https://app.dev.codelayer.gg/sessions/01a0179a-5c6f-7ab7-8e1e-92b5a54ffad3
@K-Mistele
K-Mistele merged commit 5cd3440 into main Aug 19, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant