From b0a7f222e331fcb1b087679f7ed50b812d591171 Mon Sep 17 00:00:00 2001 From: Eldon Marks Date: Sat, 19 Sep 2026 00:34:55 -0400 Subject: [PATCH] feat(harness): preserve host work-execution context --- .planning/reference/jvspatial-integration.md | 4 +- CHANGELOG.md | 16 +++++++ jvagent/harness/__init__.py | 2 + jvagent/harness/contracts.py | 46 ++++++++++++++++++++ jvagent/version.py | 2 +- pyproject.toml | 2 +- requirements-all.txt | 2 +- requirements.txt | 2 +- tests/harness/test_contracts.py | 38 ++++++++++++++++ 9 files changed, 108 insertions(+), 6 deletions(-) diff --git a/.planning/reference/jvspatial-integration.md b/.planning/reference/jvspatial-integration.md index 51ef6911..3cd7a3b2 100644 --- a/.planning/reference/jvspatial-integration.md +++ b/.planning/reference/jvspatial-integration.md @@ -7,7 +7,7 @@ ## 1. Where jvspatial lives - **Source**: `/Users/eldonmarks/Briefcase/dev/jv/jvspatial` (sibling directory in this workspace). -- **Pip install**: declared in [`pyproject.toml`](../../pyproject.toml) as `jvspatial==0.0.20`. +- **Pip install**: declared in [`pyproject.toml`](../../pyproject.toml) as `jvspatial==0.0.21`. - **Own docs**: jvspatial has its own [`README.md`](../../../jvspatial/README.md) and [`SPEC.md`](../../../jvspatial/SPEC.md). Treat those as authoritative for anything below. --- @@ -171,7 +171,7 @@ Things jvagent **owns**: ## 5. Version policy -- Minimum required jvspatial: pinned in [`pyproject.toml`](../../pyproject.toml) as `jvspatial==X.Y.Z`. Current: `==0.0.19`. +- Minimum required jvspatial: pinned in [`pyproject.toml`](../../pyproject.toml) as `jvspatial==X.Y.Z`. Current: `==0.0.21`. - When jvspatial introduces breaking changes (e.g., walker API rename, persistence shape change), bump the pin and update this section. - When adding a new dependency on a jvspatial feature, document the symbol + version it was introduced in. Helps downstream consumers know the floor. - Rationale: [`adr/0006-jvspatial-dependency.md`](../adr/0006-jvspatial-dependency.md). diff --git a/CHANGELOG.md b/CHANGELOG.md index 37e6e9d4..244853bd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -517,6 +517,22 @@ and this project adheres to [PEP 440](https://peps.python.org/pep-0440/) / ``get_access_control_action`` / ``get_action_by_type`` heal duplicates on read; graph repair uses the same keeper heuristic as bootstrap dedupe. +## [0.1.8rc15] - 2026-09-19 + +### Changed + +- **jvspatial 0.0.21.** Runtime dependency picks up + `PostgresTransaction.find_one_and_update` for durable work-kernel CAS + + outbox atomicity on one public transaction handle. + +### Added + +- **Host work-execution context helpers.** `HOST_WORK_EXECUTION_KEYS` / + `host_work_execution_context(visitor)` read host-owned fields from + `visitor.data`. Model tool payloads reject forged work-kernel authority + keys (`work_item_id`, `lease_token`, `lease_fence`, `effect_key`, + `logical_step_key`, `work_execution_context`). + ## [0.1.8rc14] - 2026-09-18 ### Changed diff --git a/jvagent/harness/__init__.py b/jvagent/harness/__init__.py index 3cb0e37d..f6de6479 100644 --- a/jvagent/harness/__init__.py +++ b/jvagent/harness/__init__.py @@ -14,6 +14,7 @@ ToolSurfaceSnapshot, TurnRunState, assert_turn_run_transition, + host_work_execution_context, native_caller_from_mapping, reject_host_domain_fields, reject_model_authority_fields, @@ -42,6 +43,7 @@ "TurnRunState", "assert_turn_run_transition", "get_runtime", + "host_work_execution_context", "native_caller_from_mapping", "reject_host_domain_fields", "reject_model_authority_fields", diff --git a/jvagent/harness/contracts.py b/jvagent/harness/contracts.py index f3a99da9..b49f294f 100644 --- a/jvagent/harness/contracts.py +++ b/jvagent/harness/contracts.py @@ -29,6 +29,34 @@ "capability_token", "snapshot_secret", "isolation_backend", + # Host-owned durable work-kernel fields. Models must not forge these + # on tool arguments; hosts may place them on visitor.data only. + "work_item_id", + "lease_token", + "lease_fence", + "effect_key", + "logical_step_key", + "work_execution_context", + } +) + +# Keys hosts may stash on walker/visitor data for resident tool callbacks. +# Kept as a frozenset so Integral (and other embeds) can round-trip a durable +# WorkExecutionContext without inventing a second channel. +HOST_WORK_EXECUTION_KEYS = frozenset( + { + "work_item_id", + "attempt", + "run_id", + "principal_id", + "workspace_id", + "logical_step_key", + "effect_key", + "lease_token", + "lease_fence", + "deadline_at", + "cancellation_signal", + "work_execution_context", } ) @@ -119,6 +147,22 @@ def reject_model_authority_fields(payload: Mapping[str, Any]) -> None: ) +def host_work_execution_context(visitor: Any) -> dict[str, Any]: + """Return host-supplied work-execution fields from ``visitor.data``. + + Model tool arguments never contribute — only the walker/visitor bag the + host populated for this turn. Missing visitor/data yields ``{}``. + """ + data = getattr(visitor, "data", None) + if not isinstance(data, Mapping): + return {} + return { + key: data[key] + for key in HOST_WORK_EXECUTION_KEYS + if key in data and data[key] is not None + } + + def assert_turn_run_transition(src: TurnRunState, dst: TurnRunState) -> None: allowed = LEGAL_TURN_RUN_TRANSITIONS.get(src, frozenset()) if dst not in allowed: @@ -263,6 +307,7 @@ async def invalidate(self, selector: SnapshotSelector) -> None: ... "CONTRACT_VERSION", "FORBIDDEN_AUTHORITY_KEYS", "FORBIDDEN_HOST_DOMAIN_KEYS", + "HOST_WORK_EXECUTION_KEYS", "EventEnvelope", "HarnessContractError", "HostCapabilityProvider", @@ -277,6 +322,7 @@ async def invalidate(self, selector: SnapshotSelector) -> None: ... "ToolSurfaceSnapshot", "TurnRunState", "assert_turn_run_transition", + "host_work_execution_context", "native_caller_from_mapping", "reject_host_domain_fields", "reject_model_authority_fields", diff --git a/jvagent/version.py b/jvagent/version.py index d403b7e0..cdf37f75 100644 --- a/jvagent/version.py +++ b/jvagent/version.py @@ -1,3 +1,3 @@ """Version information for jvagent package.""" -__version__ = "0.1.8rc14" +__version__ = "0.1.8rc15" diff --git a/pyproject.toml b/pyproject.toml index 2d941395..cfae7f71 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,7 +31,7 @@ classifiers = [ dependencies = [ "aiohttp>=3.9.0", # CI records the resolved jvspatial version after install (see .github/workflows/test-jvagent.yaml). - "jvspatial==0.0.20", + "jvspatial==0.0.21", "python-dotenv>=1.0.0", "pyyaml>=6.0.0", "httpx>=0.27.0", diff --git a/requirements-all.txt b/requirements-all.txt index 03287e8b..6c16d46c 100644 --- a/requirements-all.txt +++ b/requirements-all.txt @@ -6,7 +6,7 @@ # Must stay in sync with [project] dependencies in pyproject.toml — # enforced by tests/test_requirements_sync.py. aiohttp>=3.9.0 -jvspatial==0.0.20 +jvspatial==0.0.21 python-dotenv>=1.0.0 pyyaml>=6.0.0 httpx>=0.27.0 diff --git a/requirements.txt b/requirements.txt index 56a956ce..8fc60f38 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,7 +3,7 @@ # Test-only deps (incl. Docling for PageIndex): pyproject.toml # [project.optional-dependencies] test — install with: pip install -e ".[test]" aiohttp>=3.9.0 -jvspatial==0.0.20 +jvspatial==0.0.21 python-dotenv>=1.0.0 pyyaml>=6.0.0 httpx>=0.27.0 diff --git a/tests/harness/test_contracts.py b/tests/harness/test_contracts.py index 675c5156..d1d74a0b 100644 --- a/tests/harness/test_contracts.py +++ b/tests/harness/test_contracts.py @@ -12,6 +12,7 @@ ToolSurfaceSnapshot, TurnRunState, assert_turn_run_transition, + host_work_execution_context, native_caller_from_mapping, reject_host_domain_fields, reject_model_authority_fields, @@ -137,6 +138,43 @@ def test_model_payload_cannot_carry_authority(): reject_model_authority_fields({"q": "hi", "capability_token": "secret"}) +@pytest.mark.parametrize( + "key", + [ + "work_item_id", + "lease_token", + "lease_fence", + "effect_key", + "logical_step_key", + "work_execution_context", + ], +) +def test_model_payload_cannot_forge_work_execution_authority(key): + with pytest.raises(HarnessContractError, match="authority"): + reject_model_authority_fields({"q": "hi", key: "forged"}) + + +def test_host_work_execution_context_reads_visitor_data_only(): + class _Visitor: + data = { + "run_id": "workrun:abc", + "work_item_id": "o.WorkItem.1", + "lease_token": "tok", + "lease_fence": 3, + "noise": "ignore-me", + } + + ctx = host_work_execution_context(_Visitor()) + assert ctx == { + "run_id": "workrun:abc", + "work_item_id": "o.WorkItem.1", + "lease_token": "tok", + "lease_fence": 3, + } + assert host_work_execution_context(None) == {} + assert host_work_execution_context(object()) == {} + + def test_idempotency_classes_are_the_three_declared_ones(): assert {c.value for c in IdempotencyClass} == { "idempotent",