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
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"baseline_version": "2.7.42",
"classification_rationale": "L3-FORGE-SERIAL-CORRELATION-COMPAT-V1-20261001",
"component": "product",
"event_lineage": "L3-FORGE-SERIAL-CORRELATION-COMPAT-V1-20261001",
"expected_head": "08ad4f89f1d888747662910ad450c8070e5937c1",
"operation_id": "l3-serial-correlation-compat-20261001",
"policy_revision": "forge-bootstrap-release-cadence-v2",
"product": "forge",
"projection_paths": "product-version.json",
"release_class": "PATCH",
"requested_bump": "patch",
"requested_version": null,
"schema_version": "1",
"target_version": "2.7.43"
}
16 changes: 16 additions & 0 deletions docs/architecture/PARALLEL_ACTION_DURABLE_SLOTS_V1.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,19 @@ and always reports `target_verification: UNVERIFIED` and
`dispatch_authorized: false`. A slot never supplies correlation, admission,
or execution evidence. Installed artifact qualification and full PA-F1/F2/F3
remain separate roadmap work.

## Legacy serial correlation compatibility

Authenticated Mission detail also reports `serial_correlation` with contract
version `serial-action-correlation-compat/v1`. It reads the existing durable
singleton correlation without rewriting it. A bound view requires one
unambiguous relevant stored Action and matching direct or request-nested
Mission/Action/correlation identity. Only the Action ID and status,
correlation ID and optional host run ID are projected; prompt and
credential-bearing request fields are omitted. Missing correlation is
`UNAVAILABLE`; malformed, contradictory or multi-active records are
`UNSUPPORTED` with a reason and no selected Action. The view never implies a
new per-Action slot, EP admission, target verification, or host effect. A
future PA-F1 migration must still prove lossless persistence and
reconciliation of actual serial execution state before claiming full
multi-slot support.
2 changes: 2 additions & 0 deletions forge/operations_read_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
from .models.producer import redact_action_summary
from .parallel_action_contract import ParallelActionContractError, validate_peer_graph
from .runtime.data_root import DataRootResolver
from .serial_correlation_projection import project_serial_correlation


API_VERSION = "1"
Expand Down Expand Up @@ -286,6 +287,7 @@ def mission_detail(self, mission_id: str) -> dict[str, Any]:
"criteria": [_safe_text(item) for item in state.mission.get("acceptance_criteria", ())],
"actions": [_project_action(item) for item in state.actions],
"planning_slots": planning_slots,
"serial_correlation": project_serial_correlation(state),
"evidence_lineage": {
"execution_evidence": _project_execution_evidence(state.execution_evidence),
"execution_attempts": [_project_execution_evidence(item) for item in state.execution_history],
Expand Down
68 changes: 68 additions & 0 deletions forge/serial_correlation_projection.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
"""Read-only compatibility view of one legacy Mission execution correlation.

This never migrates state or infers an EP admission, target or execution grant.
"""
from __future__ import annotations

from collections.abc import Mapping
import re
from typing import Any


CONTRACT_VERSION = "serial-action-correlation-compat/v1"
_IN_FLIGHT = frozenset({"ACTIVE", "WAITING_FOR_RESULT"})
_IDENTIFIER = re.compile(r"[A-Za-z0-9][A-Za-z0-9._:-]{0,127}\Z")


def _safe_identifier(value: object) -> bool:
return isinstance(value, str) and _IDENTIFIER.fullmatch(value) is not None


def project_serial_correlation(state: Any) -> dict[str, Any]:
"""Bind a singleton correlation only when its Action identity is provable."""
base = {"contract_version": CONTRACT_VERSION, "mission_revision": state.revision,
"read_only": True, "binding_status": "UNAVAILABLE", "reason": "NO_CORRELATION"}
correlation = state.execution_correlation
if correlation is None:
return base
if not isinstance(correlation, Mapping):
return {**base, "binding_status": "UNSUPPORTED", "reason": "MALFORMED_CORRELATION"}
actions = state.actions
if (not isinstance(actions, (tuple, list)) or not actions
or any(not isinstance(action, Mapping) or not isinstance(action.get("id"), str)
or not action["id"] for action in actions)
or len({action["id"] for action in actions}) != len(actions)):
return {**base, "binding_status": "UNSUPPORTED", "reason": "AMBIGUOUS_ACTION_SET"}
in_flight = [action for action in actions if action.get("status") in _IN_FLIGHT]
if len(in_flight) > 1:
return {**base, "binding_status": "UNSUPPORTED", "reason": "MULTIPLE_IN_FLIGHT_ACTIONS"}
if len(in_flight) == 1:
expected_action = in_flight[0]
elif len(actions) == 1:
expected_action = actions[0]
else:
return {**base, "binding_status": "UNSUPPORTED", "reason": "AMBIGUOUS_ACTION_SET"}
request = correlation.get("request")
if request is not None and not isinstance(request, Mapping):
return {**base, "binding_status": "UNSUPPORTED", "reason": "MALFORMED_REQUEST"}
if request is not None and any(not _safe_identifier(request.get(key))
for key in ("mission_id", "action_id", "correlation_id")):
return {**base, "binding_status": "UNSUPPORTED", "reason": "MALFORMED_REQUEST"}
request = request or {}
for key in ("mission_id", "action_id", "correlation_id"):
direct, nested = correlation.get(key), request.get(key)
if direct is not None and nested is not None and direct != nested:
return {**base, "binding_status": "UNSUPPORTED", "reason": "IDENTITY_CONFLICT"}
mission_id = correlation.get("mission_id") or request.get("mission_id")
action_id = correlation.get("action_id") or request.get("action_id")
correlation_id = correlation.get("correlation_id") or request.get("correlation_id")
host_run_id = correlation.get("host_run_id")
if (mission_id != state.mission_id or not _safe_identifier(mission_id)
or action_id != expected_action["id"]
or not _safe_identifier(action_id)
or not _safe_identifier(correlation_id)
or host_run_id is not None and not _safe_identifier(host_run_id)):
return {**base, "binding_status": "UNSUPPORTED", "reason": "IDENTITY_UNVERIFIED"}
return {**base, "binding_status": "BOUND", "reason": None,
"action_id": action_id, "action_status": expected_action.get("status"),
"correlation_id": correlation_id, "host_run_id": host_run_id}
2 changes: 1 addition & 1 deletion product-version.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"product": "forge",
"schema_version": 1,
"version": "2.7.42"
"version": "2.7.43"
}
26 changes: 26 additions & 0 deletions tests/test_operations_read_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,32 @@ def test_schema40_migration_keeps_serial_mission_without_invented_slots(self) ->
self.assertEqual(response.status, 200)
self.assertIsNone(response.body["mission"]["planning_slots"])

def test_installed_mission_readback_binds_only_one_legacy_correlation(self) -> None:
self.create_mission()
document = self.database.get_document("mission_state", "MISSION-0042")
document["actions"][0]["status"] = "WAITING_FOR_RESULT"
document["execution_correlation"] = {
"request": {"mission_id": "MISSION-0042", "action_id": "ACTION-0042-A",
"correlation_id": "corr-0042", "runtime_prompt": "synthetic-private-prompt"},
"host_run_id": "run-0042",
}
self.database.save_mission_state(document)
before = self.snapshot()
response = self.api.handle("GET", "/v1/missions/MISSION-0042", "Bearer " + CREDENTIAL)
self.assertEqual(response.status, 200)
serial = response.body["mission"]["serial_correlation"]
self.assertEqual((serial["binding_status"], serial["action_id"], serial["correlation_id"]),
("BOUND", "ACTION-0042-A", "corr-0042"))
self.assertNotIn("synthetic-private-prompt", json.dumps(response.body))
self.assertEqual(self.snapshot(), before)
document["actions"][1]["status"] = "ACTIVE"
self.database.save_mission_state(document)
ambiguous = self.api.handle("GET", "/v1/missions/MISSION-0042", "Bearer " + CREDENTIAL)
self.assertEqual(ambiguous.status, 200)
self.assertEqual(ambiguous.body["mission"]["serial_correlation"]["reason"],
"MULTIPLE_IN_FLIGHT_ACTIONS")
self.assertNotIn("action_id", ambiguous.body["mission"]["serial_correlation"])

@staticmethod
def _slot_graph() -> dict:
return {
Expand Down
89 changes: 89 additions & 0 deletions tests/test_serial_correlation_projection.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
"""Legacy singleton correlation is never inferred from an ambiguous Action set."""
from __future__ import annotations

from types import SimpleNamespace
import unittest

from forge.serial_correlation_projection import project_serial_correlation


def state(actions=None, correlation=None):
return SimpleNamespace(
mission_id="MISSION-1", revision=7,
actions=({"id": "ACTION-A", "status": "WAITING_FOR_RESULT"},) if actions is None else actions,
execution_correlation=correlation,
)


class SerialCorrelationProjectionTests(unittest.TestCase):
def test_absent_correlation_is_unavailable(self) -> None:
result = project_serial_correlation(state())
self.assertEqual((result["binding_status"], result["reason"]), ("UNAVAILABLE", "NO_CORRELATION"))
self.assertEqual(result["mission_revision"], 7)

def test_direct_and_nested_request_bind_only_safe_identifiers(self) -> None:
direct = project_serial_correlation(state(correlation={
"mission_id": "MISSION-1", "action_id": "ACTION-A",
"correlation_id": "corr-a", "host_run_id": "run-a",
"secret_token": "sensitive-value",
}))
self.assertEqual(direct["binding_status"], "BOUND")
self.assertEqual(direct["action_id"], "ACTION-A")
self.assertEqual(direct["host_run_id"], "run-a")
self.assertNotIn("sensitive-value", repr(direct))
nested = project_serial_correlation(state(correlation={
"request": {"mission_id": "MISSION-1", "action_id": "ACTION-A",
"correlation_id": "corr-a", "runtime_prompt": "sensitive-prompt"},
"host_run_id": None,
}))
self.assertEqual(nested["binding_status"], "BOUND")
self.assertIsNone(nested["host_run_id"])
self.assertNotIn("sensitive-prompt", repr(nested))

def test_one_active_action_among_two_is_exactly_bound(self) -> None:
actions = ({"id": "ACTION-A", "status": "COMPLETE"},
{"id": "ACTION-B", "status": "ACTIVE"})
result = project_serial_correlation(state(actions, {
"mission_id": "MISSION-1", "action_id": "ACTION-B", "correlation_id": "corr-b",
}))
self.assertEqual((result["binding_status"], result["action_id"]), ("BOUND", "ACTION-B"))

def test_multiple_or_unselected_actions_are_unsupported(self) -> None:
two = ({"id": "ACTION-A", "status": "ACTIVE"},
{"id": "ACTION-B", "status": "WAITING_FOR_RESULT"})
result = project_serial_correlation(state(two, {"action_id": "ACTION-A", "correlation_id": "corr"}))
self.assertEqual(result["reason"], "MULTIPLE_IN_FLIGHT_ACTIONS")
idle = ({"id": "ACTION-A", "status": "READY"}, {"id": "ACTION-B", "status": "READY"})
self.assertEqual(project_serial_correlation(state(idle, {"action_id": "ACTION-A"}))["reason"],
"AMBIGUOUS_ACTION_SET")
self.assertEqual(project_serial_correlation(state((), {"action_id": "ACTION-A"}))["reason"],
"AMBIGUOUS_ACTION_SET")

def test_malformed_and_conflicting_identities_fail_closed(self) -> None:
self.assertEqual(project_serial_correlation(state(correlation=[]))["reason"], "MALFORMED_CORRELATION")
self.assertEqual(project_serial_correlation(state(correlation={"request": []}))["reason"],
"MALFORMED_REQUEST")
self.assertEqual(project_serial_correlation(state(correlation={"action_id": "ACTION-A",
"correlation_id": "corr-a", "request": {}}))["reason"],
"MALFORMED_REQUEST")
conflict = {"action_id": "ACTION-A", "correlation_id": "corr-a",
"request": {"mission_id": "MISSION-1", "action_id": "ACTION-B",
"correlation_id": "corr-a"}}
self.assertEqual(project_serial_correlation(state(correlation=conflict))["reason"], "IDENTITY_CONFLICT")
for correlation in (
{"mission_id": "MISSION-X", "action_id": "ACTION-A", "correlation_id": "corr"},
{"action_id": "ACTION-A", "correlation_id": "corr"},
{"action_id": "ACTION-X", "correlation_id": "corr"},
{"action_id": "ACTION-A", "correlation_id": ""},
{"action_id": "ACTION-A", "correlation_id": "bad value"},
{"action_id": "ACTION-A", "correlation_id": "corr", "host_run_id": []},
):
with self.subTest(correlation=correlation):
self.assertEqual(project_serial_correlation(state(correlation=correlation))["reason"],
"IDENTITY_UNVERIFIED")
duplicate = ({"id": "ACTION-A", "status": "ACTIVE"}, {"id": "ACTION-A", "status": "READY"})
self.assertEqual(project_serial_correlation(state(duplicate, {}))["reason"], "AMBIGUOUS_ACTION_SET")


if __name__ == "__main__":
unittest.main()
Loading