Source code for process_improve.simulation.context

"""(c) Kevin Dunn, 2010-2026. MIT License.

Host-side injection channel for the fake-data simulator tools.

``simulate_process`` and ``reveal_simulator`` need two pieces of state
that the LLM must never be able to supply itself:

- ``simulator_state`` - the hidden response-surface model the host
  persists per conversation.
- ``confirmed`` - whether the user has cleared the double-confirmation
  gate guarding a model reveal.

Earlier these travelled as keyword arguments deliberately kept out of
the public JSON schema, on the assumption the host would inject them
server-side. But the dispatch path forwards the tool ``input`` verbatim
as ``**kwargs``, so a prompt-injected agent could re-introduce them as
ordinary kwargs and bypass the reveal gate (SEC-15). The schema filter
in :func:`process_improve.tool_spec.execute_tool_call` now drops such
keys, and this module removes them from the kwarg surface entirely:
they live in :class:`contextvars.ContextVar` slots that only the host
populates, so they cannot be reached through a tool call even if the
filter regresses.

Host usage
----------

The host wraps each dispatch in :func:`simulator_host_context`::

    from process_improve.simulation.context import simulator_host_context
    from process_improve.tool_spec import execute_tool_call

    with simulator_host_context(simulator_state=state, confirmed=user_ok):
        result = execute_tool_call(tool_name, tool_input)

Outside such a block the context is empty, so ``simulate_process``
reports ``simulator_state_missing`` and ``reveal_simulator`` returns
``confirmation_needed`` - the safe defaults.
"""

from __future__ import annotations

import contextlib
from contextvars import ContextVar
from typing import TYPE_CHECKING, Any

if TYPE_CHECKING:
    from collections.abc import Iterator

# Hidden model state injected by the host for the current dispatch. ``None``
# means "no state available" (the default outside a host context).
_SIMULATOR_STATE: ContextVar[dict[str, Any] | None] = ContextVar("process_improve_simulator_state", default=None)

# Whether the host has cleared the reveal double-confirmation gate for the
# current dispatch. Defaults to ``False`` so an un-wrapped call never reveals.
_REVEAL_CONFIRMED: ContextVar[bool] = ContextVar("process_improve_reveal_confirmed", default=False)


[docs] def get_injected_simulator_state() -> dict[str, Any] | None: """Return the host-injected ``simulator_state`` for the current dispatch. Returns ``None`` when called outside a :func:`simulator_host_context` block, which the simulator tools surface as ``simulator_state_missing``. """ return _SIMULATOR_STATE.get()
[docs] def get_reveal_confirmed() -> bool: """Return whether the host has confirmed a reveal for the current dispatch. Defaults to ``False`` outside a :func:`simulator_host_context` block so an un-gated call can never expose the hidden model. """ return _REVEAL_CONFIRMED.get()
[docs] @contextlib.contextmanager def simulator_host_context( *, simulator_state: dict[str, Any] | None = None, confirmed: bool = False, ) -> Iterator[None]: """Inject simulator state / reveal confirmation for the enclosed dispatch. Parameters ---------- simulator_state: The hidden model the host persists for this conversation, or ``None`` to leave it unavailable. confirmed: ``True`` only after the user has cleared the reveal double-confirmation gate. Coerced to ``bool``. Notes ----- The values are stored in :class:`contextvars.ContextVar` slots and reset on exit, so concurrent hosts (e.g. async tasks) do not see each other's state. ``ContextVar`` values do not cross process boundaries, so a host that dispatches through the subprocess-isolated :func:`process_improve.tool_safety.safe_execute_tool_call` will not leak state into the worker; that untrusted-transport path is expected to report ``simulator_state_missing`` rather than carry persisted state. """ state_token = _SIMULATOR_STATE.set(simulator_state) confirmed_token = _REVEAL_CONFIRMED.set(bool(confirmed)) try: yield finally: _SIMULATOR_STATE.reset(state_token) _REVEAL_CONFIRMED.reset(confirmed_token)