fix: harden Python workflow client boundary

This commit is contained in:
lda
2026-08-31 19:51:43 +07:00 Verified
parent b377311a6e
commit 4f946a35ef
35 changed files with 1257 additions and 302 deletions
+2 -1
View File
@@ -39,7 +39,7 @@ def _capability_api(
)
service.register_specs("demo.personal", failing_tool)
context = context_from_service(service)
return WorkflowCapabilityApi(context), service
return WorkflowCapabilityApi(context, drafts=True), service
@pytest.mark.asyncio
@@ -226,6 +226,7 @@ async def test_create_draft_workspace_from_capability(tmp_path: Path) -> None:
assert "next_actions" in result
assert result["wrapper_hints"]["capability_name"] == "demo.personal.echo_tool"
assert api.drafts is not None
fetched = await api.drafts.get_draft_workspace(
workspace_id="echo_ws", include_draft=True
)
@@ -42,12 +42,13 @@ def _composite_concat_draft() -> dict[str, object]:
async def test_composite_concat_runs_through_the_platform_registry(
tmp_path: Path,
) -> None:
server = build_local_static_workflow_server(tmp_path / "store")
server = build_local_static_workflow_server(tmp_path / "store", drafts=True)
await server.api.create_draft_workspace(
workspace_id="composite_concat",
draft=_composite_concat_draft(),
)
assert server.api.draft_authoring is not None
authored = await server.api.draft_authoring.set_step_input_bindings(
workspace_id="composite_concat",
revision=1,
+31 -31
View File
@@ -171,7 +171,7 @@ async def test_inspect_draft_authoring_contract_projects_selected_capability(
"properties": {"echoed": {"type": "string"}},
}
await draft_api.create_draft_workspace(workspace_id="authoring", draft=draft)
api = WorkflowApi(authoring.context)
api = WorkflowApi(authoring.context, drafts=True)
inventory = _authoring_inventory(
await api.inspect_draft_authoring_contract(
@@ -219,7 +219,7 @@ async def test_inspect_draft_authoring_contract_tolerates_invalid_workflow_schem
"properties": {"echoed": {"type": "string"}},
}
await draft_api.create_draft_workspace(workspace_id="authoring", draft=draft)
api = WorkflowApi(authoring.context)
api = WorkflowApi(authoring.context, drafts=True)
inventory = _authoring_inventory(
await api.inspect_draft_authoring_contract(
@@ -256,7 +256,7 @@ async def test_inspect_draft_authoring_contract_resolves_saved_wrapper_capabilit
"properties": {"echoed": {"type": "string"}},
}
await draft_api.create_draft_workspace(workspace_id="authoring", draft=draft)
api = WorkflowApi(authoring.context)
api = WorkflowApi(authoring.context, drafts=True)
inventory = _authoring_inventory(
await api.inspect_draft_authoring_contract(
@@ -296,7 +296,7 @@ async def test_inspect_draft_authoring_contract_preserves_empty_capability_schem
"properties": {"echoed": {"type": "string"}},
}
await draft_api.create_draft_workspace(workspace_id="authoring", draft=draft)
api = WorkflowApi(authoring.context)
api = WorkflowApi(authoring.context, drafts=True)
inventory = _authoring_inventory(
await api.inspect_draft_authoring_contract(
@@ -344,7 +344,7 @@ async def test_inspect_draft_authoring_contract_warns_for_invalid_capability_sch
workspace_id="authoring",
draft=_echo_draft(),
)
api = WorkflowApi(authoring.context)
api = WorkflowApi(authoring.context, drafts=True)
inventory = _authoring_inventory(
await api.inspect_draft_authoring_contract(
@@ -375,7 +375,7 @@ async def test_inspect_draft_authoring_contract_rejects_unknown_selected_step(
await draft_api.create_draft_workspace(
workspace_id="authoring", draft=_echo_draft()
)
api = WorkflowApi(authoring.context)
api = WorkflowApi(authoring.context, drafts=True)
with pytest.raises(KeyError, match="unknown draft step"):
await api.inspect_draft_authoring_contract(
@@ -397,7 +397,7 @@ async def test_inspect_draft_authoring_contract_tolerates_invalid_selected_step(
draft["steps"] = {"broken": {"unknown_kind": {}}}
draft["start"] = "broken"
await draft_api.create_draft_workspace(workspace_id="authoring", draft=draft)
api = WorkflowApi(authoring.context)
api = WorkflowApi(authoring.context, drafts=True)
inventory = _authoring_inventory(
await api.inspect_draft_authoring_contract(
@@ -426,7 +426,7 @@ async def test_inspect_draft_authoring_contract_stale_revision_is_read_only(
await draft_api.create_draft_workspace(
workspace_id="authoring", draft=_echo_draft()
)
api = WorkflowApi(authoring.context)
api = WorkflowApi(authoring.context, drafts=True)
changed = await api.set_draft_name(
workspace_id="authoring",
revision=1,
@@ -1023,7 +1023,7 @@ async def _create_structured_binding_api(
workspace_id=workspace_id,
draft=_structured_report_draft(),
)
return draft_api, service, WorkflowApi(authoring.context)
return draft_api, service, WorkflowApi(authoring.context, drafts=True)
async def _create_nested_output_binding_api(
@@ -1058,7 +1058,7 @@ async def _create_nested_output_binding_api(
workspace_id=workspace_id,
draft=_nested_report_draft(),
)
return draft_api, service, WorkflowApi(authoring.context)
return draft_api, service, WorkflowApi(authoring.context, drafts=True)
@pytest.mark.asyncio
@@ -1163,7 +1163,7 @@ async def test_create_empty_draft_workspace_persists_invalid_skeleton(
) -> None:
artifact_store = FileWorkflowArtifactStore(tmp_path / "drafts_create_empty")
_drafts, _service, authoring = _draft_api(artifact_store)
facade = WorkflowApi(authoring.context)
facade = WorkflowApi(authoring.context, drafts=True)
created = await facade.create_empty_draft_workspace(
workspace_id="control_first",
@@ -1198,7 +1198,7 @@ async def test_create_empty_draft_workspace_preserves_custom_contract(
) -> None:
artifact_store = FileWorkflowArtifactStore(tmp_path / "drafts_create_contract")
_drafts, _service, authoring = _draft_api(artifact_store)
facade = WorkflowApi(authoring.context)
facade = WorkflowApi(authoring.context, drafts=True)
input_schema = {
"type": "object",
"properties": {"topic": {"type": "string"}},
@@ -1249,7 +1249,7 @@ async def test_create_empty_draft_workspace_isolates_default_schemas(
) -> None:
artifact_store = FileWorkflowArtifactStore(tmp_path / "drafts_schema_isolation")
_drafts, _service, authoring = _draft_api(artifact_store)
facade = WorkflowApi(authoring.context)
facade = WorkflowApi(authoring.context, drafts=True)
input_schema = {
"type": "object",
"properties": {"topic": {"type": "string"}},
@@ -1283,7 +1283,7 @@ async def test_create_empty_draft_workspace_reports_duplicate_conflict(
) -> None:
artifact_store = FileWorkflowArtifactStore(tmp_path / "drafts_create_conflict")
_drafts, _service, authoring = _draft_api(artifact_store)
facade = WorkflowApi(authoring.context)
facade = WorkflowApi(authoring.context, drafts=True)
await facade.create_empty_draft_workspace(
workspace_id="control_first",
name="control_first",
@@ -1317,7 +1317,7 @@ async def test_create_empty_draft_workspace_rejects_invalid_contract_before_muta
) -> None:
artifact_store = FileWorkflowArtifactStore(tmp_path / "drafts_create_rejected")
_drafts, _service, authoring = _draft_api(artifact_store)
facade = WorkflowApi(authoring.context)
facade = WorkflowApi(authoring.context, drafts=True)
with pytest.raises(ValueError):
await facade.create_empty_draft_workspace(
@@ -1335,7 +1335,7 @@ async def test_set_draft_start_and_contract_replace_top_level_fields_atomically(
) -> None:
artifact_store = FileWorkflowArtifactStore(tmp_path / "drafts_set_lifecycle")
_drafts, _service, authoring = _draft_api(artifact_store)
facade = WorkflowApi(authoring.context)
facade = WorkflowApi(authoring.context, drafts=True)
await facade.create_empty_draft_workspace(
workspace_id="control_first",
name="control_first",
@@ -1412,7 +1412,7 @@ async def test_lifecycle_edits_reject_invalid_envelopes_without_mutation(
tmp_path / f"drafts_lifecycle_rejected_{operation}"
)
_drafts, _service, authoring = _draft_api(artifact_store)
facade = WorkflowApi(authoring.context)
facade = WorkflowApi(authoring.context, drafts=True)
await facade.create_empty_draft_workspace(
workspace_id="control_first",
name="control_first",
@@ -1453,7 +1453,7 @@ async def test_lifecycle_edits_report_stale_revision_without_mutation(
tmp_path / f"drafts_lifecycle_stale_{operation}"
)
_drafts, _service, authoring = _draft_api(artifact_store)
facade = WorkflowApi(authoring.context)
facade = WorkflowApi(authoring.context, drafts=True)
await facade.create_empty_draft_workspace(
workspace_id="control_first",
name="control_first",
@@ -2187,7 +2187,7 @@ async def test_facade_delegates_semantic_authoring_to_authoring_service(
service.register_specs("demo.personal", echo_tool, _snapshot_tool)
context = context_from_service(service)
facade = WorkflowApi(context)
facade = WorkflowApi(context, drafts=True)
assert facade.draft_authoring is not None
assert isinstance(facade.draft_authoring, WorkflowDraftAuthoringApi)
@@ -3469,7 +3469,7 @@ async def test_set_step_input_bindings_rejects_remote_target_reference_without_m
workspace_id="remote_target",
draft=_structured_report_draft("demo.personal.remote_structured_report"),
)
api = WorkflowApi(authoring.context)
api = WorkflowApi(authoring.context, drafts=True)
before = await draft_api.get_draft_workspace(
workspace_id="remote_target",
include_draft=True,
@@ -3506,7 +3506,7 @@ async def test_set_step_input_bindings_rejects_non_capability_step_without_mutat
draft = _structured_report_draft()
draft["steps"]["report"] = {"join": {}}
await draft_api.create_draft_workspace(workspace_id="non_capability", draft=draft)
api = WorkflowApi(authoring.context)
api = WorkflowApi(authoring.context, drafts=True)
before = await draft_api.get_draft_workspace(
workspace_id="non_capability",
include_draft=True,
@@ -4007,7 +4007,7 @@ async def test_add_step_accepts_every_typed_draft_step(
) -> None:
artifact_store = FileWorkflowArtifactStore(tmp_path / f"draft_add_{step_name}")
draft_api, _service, authoring = _draft_api(artifact_store, register_echo=True)
api = WorkflowApi(authoring.context)
api = WorkflowApi(authoring.context, drafts=True)
await draft_api.create_draft_workspace(workspace_id="draft_ws", draft=_echo_draft())
step = TypeAdapter(DraftStep).validate_python(step_payload)
@@ -4033,7 +4033,7 @@ async def test_add_step_routes_incoming_and_outgoing_edges_atomically(
) -> None:
artifact_store = FileWorkflowArtifactStore(tmp_path / "draft_add_routes")
draft_api, _service, authoring = _draft_api(artifact_store, register_echo=True)
api = WorkflowApi(authoring.context)
api = WorkflowApi(authoring.context, drafts=True)
await draft_api.create_draft_workspace(workspace_id="draft_ws", draft=_echo_draft())
step = TypeAdapter(DraftStep).validate_python(
@@ -4062,7 +4062,7 @@ async def test_add_step_stale_revision_wins_over_content_preflight(
) -> None:
artifact_store = FileWorkflowArtifactStore(tmp_path / "draft_add_stale")
draft_api, _service, authoring = _draft_api(artifact_store, register_echo=True)
api = WorkflowApi(authoring.context)
api = WorkflowApi(authoring.context, drafts=True)
await draft_api.create_draft_workspace(workspace_id="draft_ws", draft=_echo_draft())
before = await draft_api.get_draft_workspace(
workspace_id="draft_ws", include_draft=True
@@ -4135,7 +4135,7 @@ async def test_add_step_adds_missing_incoming_route_parent_atomically(
) -> None:
artifact_store = FileWorkflowArtifactStore(tmp_path / "draft_add_missing_parent")
draft_api, _service, authoring = _draft_api(artifact_store, register_echo=True)
api = WorkflowApi(authoring.context)
api = WorkflowApi(authoring.context, drafts=True)
draft = _echo_draft()
draft["routes"] = {}
await draft_api.create_draft_workspace(workspace_id="draft_ws", draft=draft)
@@ -4164,7 +4164,7 @@ async def test_add_step_distinguishes_missing_and_explicit_empty_routes(
) -> None:
artifact_store = FileWorkflowArtifactStore(tmp_path / "draft_add_empty_routes")
draft_api, _service, authoring = _draft_api(artifact_store, register_echo=True)
api = WorkflowApi(authoring.context)
api = WorkflowApi(authoring.context, drafts=True)
await draft_api.create_draft_workspace(workspace_id="draft_ws", draft=_echo_draft())
step_adapter = TypeAdapter(DraftStep)
@@ -4207,7 +4207,7 @@ async def test_add_step_rejects_unknown_incoming_outcome_without_mutation(
) -> None:
artifact_store = FileWorkflowArtifactStore(tmp_path / "draft_add_bad_incoming")
draft_api, _service, authoring = _draft_api(artifact_store, register_echo=True)
api = WorkflowApi(authoring.context)
api = WorkflowApi(authoring.context, drafts=True)
await draft_api.create_draft_workspace(workspace_id="draft_ws", draft=_echo_draft())
draft_store = authoring.drafts._draft_store()
@@ -4277,7 +4277,7 @@ async def test_add_step_rejects_invalid_routing_inputs_atomically(
) -> None:
artifact_store = FileWorkflowArtifactStore(tmp_path / "draft_add_errors")
draft_api, _service, authoring = _draft_api(artifact_store, register_echo=True)
api = WorkflowApi(authoring.context)
api = WorkflowApi(authoring.context, drafts=True)
await draft_api.create_draft_workspace(workspace_id="draft_ws", draft=_echo_draft())
await _assert_add_step_rejected_without_mutation(
@@ -4338,7 +4338,7 @@ async def test_add_step_rejects_routes_for_non_routable_steps_atomically(
) -> None:
artifact_store = FileWorkflowArtifactStore(tmp_path / "draft_add_forbidden_routes")
draft_api, _service, authoring = _draft_api(artifact_store, register_echo=True)
api = WorkflowApi(authoring.context)
api = WorkflowApi(authoring.context, drafts=True)
await draft_api.create_draft_workspace(workspace_id="draft_ws", draft=_echo_draft())
before = await draft_api.get_draft_workspace(
@@ -4367,7 +4367,7 @@ async def test_add_step_accepts_incomplete_declared_route_subset(
artifact_store = FileWorkflowArtifactStore(tmp_path / "draft_add_partial_routes")
draft_api, service, authoring = _draft_api(artifact_store, register_echo=True)
service.register_specs("demo.personal", echo_tool, _snapshot_tool)
api = WorkflowApi(authoring.context)
api = WorkflowApi(authoring.context, drafts=True)
await draft_api.create_draft_workspace(workspace_id="draft_ws", draft=_echo_draft())
step = TypeAdapter(DraftStep).validate_python(
@@ -6300,7 +6300,7 @@ def _browser_click_api(
_collect_snapshots,
)
context = context_from_service(service)
return WorkflowApi(context), service
return WorkflowApi(context, drafts=True), service
@pytest.mark.asyncio