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
22 changes: 11 additions & 11 deletions .lint_baselines/falsey_clobber.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,16 +41,16 @@
"axonflow/client.py:850:20",
"axonflow/client.py:936:20",
"axonflow/execution.py:205:19",
"axonflow/masfeat.py:296:23",
"axonflow/masfeat.py:297:24",
"axonflow/masfeat.py:298:25",
"axonflow/masfeat.py:299:23",
"axonflow/masfeat.py:318:12",
"axonflow/masfeat.py:321:12",
"axonflow/masfeat.py:323:30",
"axonflow/masfeat.py:415:25",
"axonflow/masfeat.py:429:23",
"axonflow/masfeat.py:430:23",
"axonflow/masfeat.py:431:23"
"axonflow/masfeat.py:345:23",
"axonflow/masfeat.py:346:24",
"axonflow/masfeat.py:347:25",
"axonflow/masfeat.py:348:23",
"axonflow/masfeat.py:367:12",
"axonflow/masfeat.py:370:12",
"axonflow/masfeat.py:372:30",
"axonflow/masfeat.py:469:25",
"axonflow/masfeat.py:483:23",
"axonflow/masfeat.py:484:23",
"axonflow/masfeat.py:485:23"
]
}
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Real wire fields `policy_decision`, `policy_details`, `response_time_ms` on
the audit read model (`AuditLogEntry`), and `action` on audit search
(`AuditSearchRequest`).
- Real wire fields `org_id`, `assessments_due`, `kill_switches_triggered` on
`RegistrySummary` (#3254 pin-advance batch).
- The wire-shape contract gate now binds the masfeat dataclass models
(`RegistrySummary`/`KillSwitch`/`AISystemRegistry`) by driving their real
parsers with a key-recording payload (#3262).

### Deprecated

- `query_summary`/`success`/`blocked`/`risk_score`/`latency_ms`/
`policy_violations`/`metadata` (read model) and `request_type` (search
request) - never served/read on the 9.x line (#3254). Removal rides the
next major.
- `RegistrySummary.by_use_case`/`by_status` and
`AISystemRegistry.technical_owner` - never served on the 9.x line (#3254
pin-advance batch). Removal rides the next major.

## [9.0.0] - 2026-07-18

Expand Down
58 changes: 56 additions & 2 deletions axonflow/masfeat.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,25 @@ class Finding:

@dataclass
class AISystemRegistry:
"""Registered AI system in the MAS FEAT registry."""
"""Registered AI system in the MAS FEAT registry.

Attributes:
technical_owner: Deprecated: never populated on the 9.x line -
the server has never sent this field
(getaxonflow/axonflow-enterprise#3254); the wire carries
``owner_email`` (read into ``business_owner``) and
``owner_team``. The register/update write paths still send
it (harmless, unread server-side). Scheduled for removal in
the next major.
business_owner: Populated from the wire field ``owner_email``
(legacy spelling read first for compatibility).
customer_impact: Populated from the wire field
``risk_rating_impact`` (legacy spelling read first).
model_complexity: Populated from the wire field
``risk_rating_complexity`` (legacy spelling read first).
human_reliance: Populated from the wire field
``risk_rating_reliance`` (legacy spelling read first).
"""

id: str
org_id: str
Expand All @@ -157,7 +175,35 @@ class AISystemRegistry:

@dataclass
class RegistrySummary:
"""Summary of all AI systems in the registry."""
"""Summary of all AI systems in the registry.

Attributes:
total_systems: Total registered systems.
active_systems: Systems with status "active".
high_materiality_count: High-materiality systems. Populated from
the wire field ``high_materiality`` (legacy spelling read
first for compatibility).
medium_materiality_count: Medium-materiality systems (wire:
``medium_materiality``).
low_materiality_count: Low-materiality systems (wire:
``low_materiality``).
by_use_case: Deprecated: never populated on the 9.x line - the
server has never sent this field
(getaxonflow/axonflow-enterprise#3254); no wire equivalent
(the wire RegistrySummary serves flat counts only). Read the
flat count fields instead. Scheduled for removal in the next
major.
by_status: Deprecated: never populated on the 9.x line - the
server has never sent this field
(getaxonflow/axonflow-enterprise#3254); no wire equivalent
(the wire RegistrySummary serves flat counts only). Read
``active_systems`` and the materiality counts instead.
Scheduled for removal in the next major.
org_id: Organization the summary is scoped to (#3254 additive).
assessments_due: Systems with an assessment due (#3254 additive).
kill_switches_triggered: Kill switches currently in triggered
state (#3254 additive).
"""

total_systems: int
active_systems: int
Expand All @@ -166,6 +212,9 @@ class RegistrySummary:
low_materiality_count: int
by_use_case: dict[str, int] = field(default_factory=dict)
by_status: dict[str, int] = field(default_factory=dict)
org_id: str = ""
assessments_due: int = 0
kill_switches_triggered: int = 0


# ===========================================================================
Expand Down Expand Up @@ -321,8 +370,13 @@ def registry_summary_from_dict(data: dict[str, Any]) -> RegistrySummary:
data.get("medium_materiality_count") or data.get("medium_materiality", 0)
),
low_materiality_count=data.get("low_materiality_count") or data.get("low_materiality", 0),
# Deprecated (#3254): never served on 9.x; stay {} against real servers.
by_use_case=data.get("by_use_case", {}),
by_status=data.get("by_status", {}),
# #3254 additive: real wire fields the model previously lacked.
org_id=data.get("org_id", ""),
assessments_due=data.get("assessments_due", 0),
kill_switches_triggered=data.get("kill_switches_triggered", 0),
)


Expand Down
104 changes: 104 additions & 0 deletions runtime-e2e/masfeat_real_wire_fields/test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
"""Real-stack assertion: RegistrySummary's real wire fields (#3254
pin-advance batch) parse from a live masfeat registry-summary response.

Drives the real SDK's `masfeat_get_registry_summary()` against a real
running agent and asserts the TYPED dataclass:
- the #3254 additions (org_id/assessments_due/kill_switches_triggered)
and the correct-by-fallback materiality counts parse from the real
wire names (high_materiality etc., masfeat/types.go @ v9.13.0);
- the deprecated fiction fields (by_use_case/by_status) stay {} on a
real response - the server has never sent them on 9.x.

Posture note: the masfeat surface is Enterprise-gated. On a COMMUNITY
deployment the route 404s; that outcome is DIAGNOSED (the stack must
still prove reachable via /health through the same client) and reported
as GATED, not silently skipped and not treated as a pass of the
assertions above. Run against an Enterprise stack for full coverage.

Usage::

export AXONFLOW_AGENT_URL=http://localhost:8080
export AXONFLOW_TENANT_ID=<client id>
export AXONFLOW_TENANT_SECRET=<secret>
python runtime-e2e/masfeat_real_wire_fields/test.py
"""

from __future__ import annotations

import asyncio
import os
import sys

from axonflow import AxonFlow
from axonflow.exceptions import AxonFlowError

AGENT_URL = os.environ.get("AXONFLOW_AGENT_URL", "http://localhost:8080")
CLIENT_ID = os.environ.get("AXONFLOW_TENANT_ID", "demo-client")
SECRET = os.environ.get("AXONFLOW_TENANT_SECRET", "demo-secret")


def _fail(msg: str) -> None:
sys.stderr.write(f"FAIL: {msg}\n")
sys.exit(1)


async def main() -> int:
async with AxonFlow(
endpoint=AGENT_URL,
client_id=CLIENT_ID,
client_secret=SECRET,
) as client:
try:
summary = await client.masfeat_get_registry_summary()
except AxonFlowError as exc:
if "404" not in str(exc):
_fail(f"masfeat_get_registry_summary failed non-404: {exc}")
# Diagnose, don't skip: the 404 must come from a live stack.
if not await client.health_check():
_fail(
f"masfeat route 404 AND /health not healthy at {AGENT_URL} - "
"that is an unreachable/broken stack, not a gated surface"
)
print(
"GATED: masfeat routes are not served by this deployment "
f"(HTTP 404 at {AGENT_URL}, /health healthy) - the masfeat "
"surface is Enterprise-gated; run this suite against an "
"Enterprise stack for full coverage. Diagnosed, not skipped."
)
return 0

# Enterprise path: typed assertions on the real wire shape.
if summary.total_systems < 0:
_fail(f"total_systems parsed negative: {summary.total_systems}")
counts = (
summary.high_materiality_count
+ summary.medium_materiality_count
+ summary.low_materiality_count
)
if counts > summary.total_systems:
_fail(
f"materiality counts {counts} exceed total_systems "
f"{summary.total_systems} - real-name fallback parse suspect"
)
if not isinstance(summary.assessments_due, int) or not isinstance(
summary.kill_switches_triggered, int
):
_fail("#3254 additions did not parse as ints")
if summary.by_use_case != {} or summary.by_status != {}:
_fail(
"deprecated fiction fields populated on a real response: "
f"by_use_case={summary.by_use_case!r} by_status={summary.by_status!r} "
"- the 9.x server never sends them; if a future server does, "
"revisit the #3254 deprecation before shipping"
)
print(
f"PASS: RegistrySummary parsed from live stack: org_id={summary.org_id!r} "
f"total={summary.total_systems} active={summary.active_systems} "
f"assessments_due={summary.assessments_due} "
f"kill_switches_triggered={summary.kill_switches_triggered}"
)
return 0


if __name__ == "__main__":
sys.exit(asyncio.run(main()))
27 changes: 23 additions & 4 deletions scripts/refresh_wire_shape_baseline.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,16 @@
TEST_MODULE_PATH = REPO_ROOT / "tests" / "test_wire_shape.py"
BASELINE_PATH = REPO_ROOT / "tests" / "fixtures" / "wire_shape_baseline.json"

# Bind ``import axonflow`` to THIS repo's package, ahead of any installed
# (or editable-installed-from-elsewhere) copy on sys.path. Without this,
# ``python scripts/refresh_wire_shape_baseline.py`` puts scripts/ (not the
# repo root) at sys.path[0], so a stale editable install pointing at a
# DIFFERENT checkout silently wins and the regenerated baseline records
# that other tree's models - observed in practice (#3254 batch 2): a
# sibling checkout's pre-fix masfeat parser produced a wrong-but-plausible
# drift entry with no error.
sys.path.insert(0, str(REPO_ROOT))


def _load_test_helpers():
spec = importlib.util.spec_from_file_location("_ws", TEST_MODULE_PATH)
Expand Down Expand Up @@ -129,14 +139,14 @@ def main() -> int:

registered: list[str] = []
drift: dict[str, dict[str, Any]] = {}
for name, model in models.items():

def _record(name: str, sdk_fields: list[str]) -> None:
if name not in merged:
continue
return
registered.append(name)
sdk_fields = helpers._wire_fields(model)
spec_fields = merged[name]
if sdk_fields == spec_fields:
continue
return
entry: dict[str, Any] = {
"sdk_only": sorted(set(sdk_fields) - set(spec_fields)),
"spec_only": sorted(set(spec_fields) - set(sdk_fields)),
Expand All @@ -145,6 +155,15 @@ def main() -> int:
entry["note"] = existing_notes[name]
drift[name] = entry

for name, model in models.items():
_record(name, helpers._wire_fields(model))

# #3262: masfeat dataclass bindings (parser-consumed wire keys) join
# the baseline on the same terms as pydantic models, so a pin bump
# regen recomputes their drift instead of silently dropping it.
for name, consumed in helpers._masfeat_dataclass_bindings().items():
_record(name, consumed)

cross_spec: dict[str, dict[str, list[str]]] = {
name: {spec: list(fields) for spec, fields in sorted(decls.items())}
for name, decls in sorted(duplicates_by_spec.items())
Expand Down
32 changes: 32 additions & 0 deletions tests/fixtures/wire_shape_baseline.json
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,16 @@
},
"openapi_specs_sha": "0bd9256237ebbffb9c0101126da71f2c940a1695",
"per_model_drift": {
"AISystemRegistry": {
"note": "acknowledged-sdk-superset: getaxonflow/axonflow-enterprise#3254 pin-advance batch (dataclass binding, #3262) - the parser reads the REAL server wire names (platform/orchestrator/masfeat/types.go at v9.13.0) ahead of this fiction-era spec pin, which still declares the legacy shape. Resolves when the pin advances to v9.13.0 (PR #214), where the legacy spellings the parser reads first-for-compatibility become the sdk_only set instead. owner_email/risk_rating_impact/risk_rating_complexity/risk_rating_reliance are the real wire names read as fallbacks behind the legacy business_owner/customer_impact/model_complexity/human_reliance spellings; technical_owner is deprecated fiction (never served on 9.x).",
"sdk_only": [
"owner_email",
"risk_rating_complexity",
"risk_rating_impact",
"risk_rating_reliance"
],
"spec_only": []
},
"AuditLogEntry": {
"note": "spec-bug-pending: #1745 \u2014 agent-api.yaml AuditLogEntry omits metadata/model/policy_violations the agent emits on every audit-log read. Plus getaxonflow/axonflow-enterprise#3254 additive interim: policy_decision/policy_details/response_time_ms are the REAL 9.x wire fields (platform/orchestrator/audit_logger.go AuditEntry serves them at v9.6.1 and v9.13.0); this pre-v9 spec pin predates them, so they read as sdk_only until the pin moves to v9.13.0 (PR #214).",
"sdk_only": [
Expand Down Expand Up @@ -307,6 +317,13 @@
],
"spec_only": []
},
"KillSwitch": {
"note": "acknowledged-sdk-superset: getaxonflow/axonflow-enterprise#3254 pin-advance batch (dataclass binding, #3262) - the parser reads the REAL server wire names (platform/orchestrator/masfeat/types.go at v9.13.0) ahead of this fiction-era spec pin, which still declares the legacy shape. Resolves when the pin advances to v9.13.0 (PR #214), where the legacy spellings the parser reads first-for-compatibility become the sdk_only set instead. triggered_reason is the legacy first-choice read; trigger_reason (masfeat/types.go:288) is the real wire name.",
"sdk_only": [
"trigger_reason"
],
"spec_only": []
},
"MCPCheckInputRequest": {
"note": "acknowledged-sdk-superset: tracked in #2563/#2571 \u2014 SDK declares `content_type` (request-redaction detector selector, ADR-056); the agent consumes it on POST /api/v1/mcp/check-input but agent-api.yaml doesn't yet declare it. Also declares `tool` (epic #2905, platform sub-issue #2904) \u2014 the two-field (server, tool) identity contract; #2904 merged to axonflow-enterprise (c8df2006b) and first released in platform v9.10.0, so this drift just tracks the pinned OpenAPI spec (agent-api.yaml) catching up.",
"sdk_only": [
Expand Down Expand Up @@ -378,6 +395,18 @@
],
"spec_only": []
},
"RegistrySummary": {
"note": "acknowledged-sdk-superset: getaxonflow/axonflow-enterprise#3254 pin-advance batch (dataclass binding, #3262) - the parser reads the REAL server wire names (platform/orchestrator/masfeat/types.go at v9.13.0) ahead of this fiction-era spec pin, which still declares the legacy shape. Resolves when the pin advances to v9.13.0 (PR #214), where the legacy spellings the parser reads first-for-compatibility become the sdk_only set instead. Real fields org_id/assessments_due/kill_switches_triggered added by this batch; by_use_case/by_status are deprecated fiction (never served on 9.x), spec-declared only by this fiction-era pin.",
"sdk_only": [
"assessments_due",
"high_materiality",
"kill_switches_triggered",
"low_materiality",
"medium_materiality",
"org_id"
],
"spec_only": []
},
"ResumePlanResponse": {
"note": "spec-bug-pending: #1745 \u2014 orchestrator-api.yaml ResumePlanResponse omits 7 fields returned on every WCP step approval/resume.",
"sdk_only": [
Expand Down Expand Up @@ -432,6 +461,7 @@
}
},
"registered_models": [
"AISystemRegistry",
"AuditLogEntry",
"AuditSearchRequest",
"AuditToolCallRequest",
Expand All @@ -458,6 +488,7 @@
"ExfiltrationCheckInfo",
"ExplainPolicy",
"ExplainRule",
"KillSwitch",
"LLMProviderListResponse",
"ListWorkflowsResponse",
"MCPCheckInputRequest",
Expand All @@ -483,6 +514,7 @@
"PolicyVersion",
"PricingInfo",
"RateLimitInfo",
"RegistrySummary",
"ResumeFromCheckpointResponse",
"ResumePlanResponse",
"RetryContext",
Expand Down
Loading
Loading