624a752e79
- deadline: _result()/_abandon() replace 7 BoundedResult constructions and 2 cancel+callback sites; timers handled as a list; dead raise_if_timed_out removed. - file_safety: retired classify_cross_profile_target (0 refs; get_cross_profile_warning stub kept for external callers), _home_and_resolved/_mirror_warning shared by the sandbox/container mirror guards; _find_sandbox_mirror_segments inlined. - estop: _hermes_home/_canonical_root now the file_safety helpers; _reset_log_state_for_tests inlined into its only test. - subagent_lifecycle: _validate_request driven by _UNSUPPORTED_REQUEST_FIELDS table. - turn_liveness: dead start()/_abort_message removed, _emit_warning shared. - process_bootstrap: _enable_happy_eyeballs reused by the client variant. - Comment/docstring compaction across the remaining leaf modules.
114 lines
4.3 KiB
Python
114 lines
4.3 KiB
Python
"""Context-local state for delegate_task child execution.
|
|
|
|
A Hermes process may itself be a Kanban dispatcher worker with HERMES_KANBAN_*
|
|
in os.environ. delegate_task children (in-process) and cron jobs fired via
|
|
``cronjob(action="run")`` are NOT dispatcher-owned, so identity gates must
|
|
fail closed for them without mutating the process-global environment.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
from contextlib import contextmanager
|
|
from contextvars import ContextVar, Token
|
|
from typing import Iterator, Mapping, MutableMapping
|
|
|
|
_DELEGATED_CHILD_CONTEXT: ContextVar[bool] = ContextVar("hermes_delegated_child_context", default=False)
|
|
|
|
# Any in-process execution that is NOT the dispatcher-owned worker (cron jobs).
|
|
# Kept separate from _DELEGATED_CHILD_CONTEXT so delegate_task-specific
|
|
# behaviour (subprocess env scrubbing, its error strings) is unchanged.
|
|
_NON_DISPATCHER_OWNED_CONTEXT: ContextVar[bool] = ContextVar("hermes_non_dispatcher_owned_context", default=False)
|
|
|
|
DELEGATED_CHILD_ENV_MARKER = "HERMES_DELEGATED_CHILD_CONTEXT"
|
|
|
|
KANBAN_ENV_KEYS: tuple[str, ...] = (
|
|
"HERMES_KANBAN_TASK",
|
|
"HERMES_KANBAN_RUN_ID",
|
|
"HERMES_KANBAN_WORKSPACE",
|
|
"HERMES_KANBAN_WORKSPACES_ROOT",
|
|
"HERMES_KANBAN_CLAIM_LOCK",
|
|
"HERMES_KANBAN_BOARD",
|
|
"HERMES_KANBAN_DB",
|
|
)
|
|
|
|
|
|
@contextmanager
|
|
def delegated_child_context(session_id: str | None = None) -> Iterator[None]:
|
|
"""Mark child execution and isolate its task-local session identity.
|
|
|
|
Even a context entered without an id must restore the parent's session
|
|
ContextVar (child construction calls ``set_current_session_id``).
|
|
"""
|
|
token = _DELEGATED_CHILD_CONTEXT.set(True)
|
|
try:
|
|
# Lazy: session_context calls is_delegated_child_context().
|
|
from gateway.session_context import scoped_current_session_id
|
|
|
|
with scoped_current_session_id(session_id):
|
|
yield
|
|
finally:
|
|
_DELEGATED_CHILD_CONTEXT.reset(token)
|
|
|
|
|
|
def is_delegated_child_context() -> bool:
|
|
"""Return True while code is running for a delegate_task child."""
|
|
return bool(_DELEGATED_CHILD_CONTEXT.get())
|
|
|
|
|
|
def enter_non_dispatcher_owned_context() -> Token[bool]:
|
|
"""Token form of :func:`non_dispatcher_owned_context` for long try/finally scopes."""
|
|
return _NON_DISPATCHER_OWNED_CONTEXT.set(True)
|
|
|
|
|
|
def exit_non_dispatcher_owned_context(token: Token[bool]) -> None:
|
|
"""Restore the flag saved by :func:`enter_non_dispatcher_owned_context`."""
|
|
_NON_DISPATCHER_OWNED_CONTEXT.reset(token)
|
|
|
|
|
|
@contextmanager
|
|
def non_dispatcher_owned_context() -> Iterator[None]:
|
|
"""Mark in-process execution that does NOT own the dispatcher's Kanban task.
|
|
|
|
Without it a cron agent run inside a worker is misread as that worker
|
|
(kanban toolset force-added, ``kanban_complete`` defaulting to its task).
|
|
ContextVar-scoped rather than clearing os.environ, which the worker's claim
|
|
heartbeat and concurrent readers share.
|
|
"""
|
|
token = enter_non_dispatcher_owned_context()
|
|
try:
|
|
yield
|
|
finally:
|
|
exit_non_dispatcher_owned_context(token)
|
|
|
|
|
|
def is_dispatcher_owned_worker_context() -> bool:
|
|
"""The single predicate every ``HERMES_KANBAN_*`` identity gate should use."""
|
|
return not (_DELEGATED_CHILD_CONTEXT.get() or _NON_DISPATCHER_OWNED_CONTEXT.get())
|
|
|
|
|
|
def is_delegated_child_process_context() -> bool:
|
|
"""Return True in this process or a subprocess spawned by a child."""
|
|
return bool(_DELEGATED_CHILD_CONTEXT.get()) or bool(os.environ.get(DELEGATED_CHILD_ENV_MARKER))
|
|
|
|
|
|
def scrub_kanban_env(env: Mapping[str, str] | MutableMapping[str, str]) -> dict[str, str]:
|
|
"""Return *env* with dispatcher-only Kanban variables removed."""
|
|
cleaned = dict(env)
|
|
for key in KANBAN_ENV_KEYS:
|
|
cleaned.pop(key, None)
|
|
cleaned[DELEGATED_CHILD_ENV_MARKER] = "1"
|
|
return cleaned
|
|
|
|
|
|
def delegated_child_subprocess_env(
|
|
env: Mapping[str, str] | MutableMapping[str, str] | None = None,
|
|
) -> dict[str, str] | None:
|
|
"""Return an env override only when delegated-child lineage must cross fork.
|
|
|
|
Preserves ``env=None`` inherit semantics for non-delegated calls; in a
|
|
child, materializes a scrubbed env carrying the lineage marker.
|
|
"""
|
|
if not is_delegated_child_process_context():
|
|
return None if env is None else dict(env)
|
|
return scrub_kanban_env(os.environ if env is None else env)
|