Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .planning/reference/jvspatial-integration.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

---
Expand Down Expand Up @@ -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).
Expand Down
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions jvagent/harness/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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",
Expand Down
46 changes: 46 additions & 0 deletions jvagent/harness/contracts.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
}
)

Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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",
Expand All @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion jvagent/version.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
"""Version information for jvagent package."""

__version__ = "0.1.8rc14"
__version__ = "0.1.8rc15"
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion requirements-all.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
38 changes: 38 additions & 0 deletions tests/harness/test_contracts.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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",
Expand Down
Loading