Repository navigation
feat(typed-policies): v11 parity: typed members, active(), v11 legs - #264
Merged
Merged
Conversation
The typed answers carry every member a v11.0.0 platform sends: the refusal's policy, the edition's catalog_digest, registry_version and catalog_fixture, a TemplateOmissionReport on the publication and the activation, and the system control's name; mandatory is a plain bool. active() returns None only for the platform's 404 nothing_active. The route-deprecation record keys on the route template for a path that carries an id: both request helpers take path_id and build the path from the template, and a census covers the platform's seven deprecated families. The simulation methods' notes carry the ruled wording and drop the typed simulate that does not exist. The handshake texts say the engine refuses on every edition. The typed-policies example finds its default document from its own location, prints the omission report before it activates, and fails the run on a refused publication or activation; runtime-e2e/v11_examples runs both examples from outside the tree, and the typed_policies leg asserts the new members live. Signed-off-by: Saurabh Jain <saurabh.jain@getaxonflow.com>
First review folds. No SDK release has shipped typed policies, so the docs no longer describe a release that read any 404 as nothing active; active() keeps None only for nothing_active and cites getaxonflow/axonflow-enterprise#4255. The v11_examples leg copies the package sources out of the tree before installing, checks by real path that the examples import the SDK from its virtual environment, runs them with no PYTHONPATH and turns telemetry off. The typed_policies leg asserts registry_version. _route_with_id cites #263. Issue references use their full form. Signed-off-by: Saurabh Jain <saurabh.jain@getaxonflow.com>
Typed policy authoring is new in this release, so the CHANGELOG describes its members, active() and the example's behaviour in the entries for the feature and the examples rather than as a separate change. The v11_examples leg unsets PYTHONPATH once, exits 2 when a virtual environment cannot be built, and checks where the SDK imports from with the examples' own directory first on sys.path, reporting an import failure as such. Signed-off-by: Saurabh Jain <saurabh.jain@getaxonflow.com>
The baseline keys each pre-existing finding by file, line and column, and the route-template helper and docstrings added to axonflow/client.py moved 15 of them down the file. Each entry's source text is unchanged from main (verified line by line); no new finding is added. Rebuilt by the script's own --write-baseline. Signed-off-by: Saurabh Jain <saurabh.jain@getaxonflow.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
v11 parity for the v11.0.0 SDK train (#262): the typed policy answers carry every member a v11.0.0 platform sends,
active()keys on the platform's reason, the v11 examples say what activation does and run as a live leg, and the rest of #260 lands with it. The Go SDK carries the same shape (getaxonflow/axonflow-sdk-go#239), and so does the Rust SDK (getaxonflow/axonflow-sdk-rust#100 and #101); the cross-SDK rows are on getaxonflow/axonflow-enterprise#4249.axonflow/typed_policies.py,axonflow/exceptions.py):TypedPolicyRefusal.policy, the policy a tier refusal names (keyword-only, like the refusal's other members);TypedAuthoringEdition.catalog_digest,registry_versionandcatalog_fixture, the vocabulary's identity;TemplateOmissionReport(omitted, of, message), astemplate_omissionsandtemplate_omissions_unavailableon both the publication and the activation (the platform's shape,platform/decision/activation/restriction.go:217-221), beside the activation record rather than inside it;TypedPolicySystemControl.name.TypedPolicySystemControl.mandatoryis now a plainbool,Falsewhen the platform omits it or sendsnull, where it wasbool | None. Typed authoring is unreleased ([Unreleased]in the CHANGELOG), so no released caller sees the type change.TemplateOmissionReportis exported fromaxonflowandaxonflow.typed_policies.The platform's spec does not declare these members yet: getaxonflow/axonflow-enterprise#4262.
active()returnsNoneonly for a 404 whose reason isnothing_active. Any other 404, from a platform before v11.0.0 or an endpoint that is not an agent, raisesTypedPolicyRefusalwith status 404. The docs cite getaxonflow/axonflow-enterprise#4255 (the platform also answersnothing_activefor a store read failure). No SDK release has shipped typed policies, so this is not a change to released behaviour. D6: v11 parity (v11.0.0 SDK train) #262 item 2 asked the docs to say "An SDK release from before this change read any 404 as nothing active." That sentence is not carried: no release could have done so, and the release owner ruled it out for every SDK, with the cross-SDK evidence comment and the other SDKs' parity issues amended to match.examples/typed_policies.pyfinds its default document from its own location, so it runs from any directory. It prints the publication's template-omission report before it activates, and a refused publication or activation it asked for fails the run, printing the platform's reason for either;examples/pep_handshake.pycarries the #4247 follow-on after itsunknown_constraintsentence, and says the engine refuses on every edition;unknown_constraintsentence and its follow-on.policy, and theactive()bullet.Closes #260
Closes #261
Closes #262
Part of getaxonflow/axonflow-enterprise#3746
#260, item by item
Each item below is #260's body or one of its comments.
runtime-e2e/v11_examples/run.shcopies the package sources to a temporary directory, installs the SDK from there into a virtual environment (so nothing is built inside the tree), checks that the examples' interpreter imports it from that environment, and runs both examples with it, from that directory, with noPYTHONPATH, on a live Community stack, in the README's order. It replaces the[skip-runtime-e2e]hatch the examples shipped with. See Live legs./api/v1/typed-policies, which ships with the v11 series" is gone fromsimulate_policieson both clients: no such route exists.simulate_policies,get_policy_impact_reportanddetect_policy_conflicts, on the asynchronous and the synchronous client), the README and the CHANGELOG say each method keeps answering until v11.1, and that on a v11.0.0 platform its result comes from the legacy engine, which no longer decides, so it does not predict what the platform enforces. They point at the typed policy methods./api/v1/static-policies/{id}, its/versionsand/override, and/api/v1/dynamic-policies/{id}were reported once per id.path_id;_requestand_orchestrator_requestbuild the path from the template, so the template and the path cannot disagree, and_warn_if_route_deprecatedkeys on the template. The Java SDK's route-deprecation record is the model. The id is still sent as given, not escaped: a pre-existing gap, filed as Id-bearing policy paths are not escaped #263.%-formatting orstr.format, with a planted case of each per family, and pins the eleven template sites.test_a_refused_stamped_request_is_still_reported_oncechanges with it, the intended behaviour change: it expected the per-id keyGET /api/v1/static-policies/pol_missing, and expects the templateGET /api/v1/static-policies/{id}.AXONFLOW_CLIENT_IDtocommunity.axonflow/pep_handshake.pyandruntime-e2e/pep_handshake_planes/README.mdno longer say a Community deployment records the declaration and does not deny, and the README's handshake bullet ("Which edition refuses what") andexamples/pep_handshake.pysay the same. On every edition the engine refuses a mandatory obligation the declaration cannot discharge (composeSet); what only Enterprise adds happens at the handler.#261: after every copy of the
unknown_constraintsentence (examples/pep_handshake.py,examples/typed_policies.py, the README,runtime-e2e/v11_examples): "From v11.0.0 the deny's first reason is that code, followed by one naming each constraint it could not evaluate and the attribute it needed (getaxonflow/axonflow-enterprise#4247)."Tests
New tests, one per guard, each proven to run by name (
pytest -rA,PASSED <node id>, in the mutant run's baseline below):tests/test_typed_policies.py:test_a_404_that_is_not_nothing_active_is_a_typed_refusal(a plain-text 404 and a 404 with another reason)test_the_edition_names_its_vocabularytest_the_publication_carries_the_template_omission_reporttest_a_publication_whose_omission_report_is_unavailable_says_whytest_the_activation_carries_the_omission_report_beside_the_recordtest_an_activation_whose_omission_report_is_unavailable_says_whytest_a_system_control_names_itself_and_is_not_mandatory_when_unsaid(absent andnullboth readFalse, compared by value so a drift back toNonefails)TestTheExample::test_the_example_finds_its_default_body_from_its_own_locationtests/test_legacy_deprecations.py,TestOncePerTemplate:test_a_stamped_route_with_an_id_is_reported_once_not_once_per_idtest_every_id_bearing_method_reports_its_template_oncetest_no_id_bearing_path_is_built_outside_its_templateChanged: the refusal table's tier row carries
policy, and every row assertsrefusal.policy(the outage row names none); the null table gains"template_omissions": nulland"omitted": null;test_a_refused_stamped_request_is_still_reported_once, above.Local round
The head,
7398589, differs from1595df3only in the wording of its last commit's message; the two trees are identical (0f88682f6300), so every result in this description, run at1595df3, holds for the head.Every step at the head
1595df3, each under its own build-slot ticket, in a Python 3.11 virtual environment with the SDK installed editable from this tree (checked before the round), mirroring the required contexts. Before each step the round checks that the head is unchanged and the tree clean.ruff check .andruff format --check .mypy axonflow(strict)scripts/lint_no_falsey_clobber.py axonflow/ --baseline .lint_baselines/falsey_clobber.jsonscripts/gen_authzen_types.py --checkpytest, with the configured--cov-fail-under=75: 85.80%scripts/lint-no-mocks-in-runtime-e2e.shpytest tests/test_wire_shape.py -m wire_shapeagainsttests/fixtures/openapi(unchanged; no baseline move)py_compileand import of both examplesThe falsey-clobber baseline moves 15 entries, and adds none.
.lint_baselines/falsey_clobber.jsonkeys each pre-existing finding by file, line and column. The route-template helper and docstrings added toaxonflow/client.pymoved 15 of those findings down the file, so the lint reported them as 15 new findings and 15 stale entries. Before regenerating, each of the 15 was checked: its source text is identical tomain's text at the stale line number, and matches the line it is now reported at. The only lines this change adds underaxonflow/that containorare docstring prose. The baseline was rebuilt by the script's own--write-baseline: the diff is exactly 15client.pyentries moved, and the check passes with 51 findings, all baselined, the same count as before.Mutants
Each mutant is an exact-once string replacement in the source, restored from saved bytes after every mutant (never
git checkout). It counts as KILLED only when its named test node ran and failed (pytest -rA,FAILED <node id>); a mutant that does not import or collect would score INVALID, and a named node that did not run would score ERROR. The unmutated baseline must pass each named node by name first: 14 nodes passed. Before running, the harness prints the token each mutant changes, so a mutant named for one token cannot quietly move another. 24 of 24 KILLED at1595df3.A harness note. The first run stopped at its baseline on a bug in the harness, not the tests. The pattern that finds a node's
PASSEDline ended in a word boundary,\b, which never matches after a parameter id's closing], so every bracketed node read as not passed; the test's own log showed it passing. The pattern now ends at whitespace or the end of the line. Before the run above, it was checked against that log and three planted cases: a node that is a prefix of a longer name does not match the longer one; a node without a parameter id matches each of its cases; a bracketed node matches only its own case.test_a_404_that_is_not_nothing_active_is_a_typed_refusal(both cases)no_active_policytest_nothing_active_is_none_reason()returnsNonetest_nothing_active_is_none_raise_for_refusalstops passingpolicytest_every_documented_refusal_is_typed[publish-402-tier-limit]TypedPolicyRefusalstoresNoneforpolicytest_every_documented_refusal_is_typed[publish-402-tier-limit]catalog_digest->catalog_digeststest_the_edition_names_its_vocabularyregistry_version->registry_versionstest_the_edition_names_its_vocabularycatalog_fixture->catalog_fixturestest_the_edition_names_its_vocabularytemplate_omissions->template_omissiontest_the_publication_carries_the_template_omission_reporttemplate_omissions_unavailable->template_omission_unavailabletest_a_publication_whose_omission_report_is_unavailable_says_whytemplate_omissions->template_omissiontest_the_activation_carries_the_omission_report_beside_the_recordtemplate_omissions_unavailable->template_omission_unavailabletest_an_activation_whose_omission_report_is_unavailable_says_whyomittedno longer readsnullas emptytest_a_nil_go_collection_reads_as_empty[publish-template-omissions-omitted]name->namestest_a_system_control_names_itself_and_is_not_mandatory_when_unsaidmandatory: bool | None = Noneagaintest_a_system_control_names_itself_and_is_not_mandatory_when_unsaidnullis no longer read asFalsetest_a_system_control_names_itself_and_is_not_mandatory_when_unsaidtest_a_stamped_route_with_an_id_is_reported_once_not_once_per_id_requestfills the path but forgets the templatetest_a_stamped_route_with_an_id_is_reported_once_not_once_per_id_orchestrator_requestfills the path but forgets the templatetest_every_id_bearing_method_reports_its_template_once_route_with_iddrops the idtest_every_id_bearing_method_reports_its_template_once{id}/versionstemplate loses/versionstest_every_id_bearing_method_reports_its_template_oncetoggle_dynamic_policysendsPATCHtest_every_id_bearing_method_reports_its_template_onceDELETEcall site goes back to an f-string pathtest_no_id_bearing_path_is_built_outside_its_templatefixtures/test_the_example_finds_its_default_body_from_its_own_locationLive legs
Both legs ran at the head
1595df3, each on its own fresh Community stack. The stacks were built from axonflow-enterprise857455033, with the agent and orchestrator on the application database role (the boot probe passed 3 of 3, with every pool onaxonflow_app_role). Neither used an enterprise licence or secret. Each stack was torn down by its compose project label, leaving 0 containers and 0 volumes by label.runtime-e2e/typed_policies: PASS, 23 assertions, 0 failed. What it read live:active()isNonebefore anything is activated: the platform's404 nothing_active.catalog_digest=sha256:8eec9ebc…,registry_version=2,catalog_fixture=False.system()returns 118 controls: 118 named, 25 mandatory, and everymandatoryabool.template omissions: 22 of 22, and the activation reports the same omissions.Client/axonflow-api-credential/runtime-e2e, not the name in the request.409 activation_refused("refusing to promote version 1 over active version 1").422 publication_refused.ACTION_NOT_REGISTEREDfinding on validate, and422 document_refusedcarrying it on publish.runtime-e2e/v11_examples: PASS, 13 assertions, 0 failed, after thecurlprecondition answered404 nothing_active. The SDK was installed from a copy of this tree's sources into a virtual environment in a temporary directory, and the check of where the examples import it from found that environment (…/venv/lib/python3.11/site-packages/axonflow). Every example ran from that directory, with no body file and noPYTHONPATH:pep_handshake.py: exit 0. The first decide wasverdict=allow, and the declaration the platform would refuse failed in the client, at/pep_id, before anything was sent.typed_policies.pywithout publishing: exit 0,nothing is active.AXONFLOW_TYPED_POLICY_PUBLISH=1: exit 0. It printedtemplate omissions: 22 of 22 template controls: ...beforeactivated.refused: HTTP 422 document_refused: ....published sha256:c6c7cef2... (version 1), thenactivation refused: HTTP 409 activation_refused: authoring: refusing to promote version 1 over active version 1; ..., exit 1.verdict=deny obligations=0 reasons=['unknown_constraint']. The stack is built from857455033, which predates getaxonflow/axonflow-enterprise#4247, so the deny carries only the bare code.R3
Two rounds by a read-only reviewer that built and ran nothing, each checked against the platform source (axonflow-enterprise at
857455033) and against the Go SDK's merged equivalent (getaxonflow/axonflow-sdk-go#239).Round 1, at
e47563a: NOT-SHIP-READY, with no code defect. Folded at760a7d2:PYTHONPATHunset, compares real paths, and fails unless the SDK comes from the virtual environment; every example runs with noPYTHONPATH.pip installof the tree built inside it. The leg now installs from a copy of the package sources in the temporary directory.registry_versionwas printed, not asserted, in the typed leg. It is asserted.Not folded, by ruling: #261 also suggested pointing callers at the deny's second reason and first evaluated policy. The ordering is the platform's documented contract, not something this SDK's docs should promise again, so #261 closes on the #4247 sentence alone.
Round 2, at
760a7d2: SHIP-READY. It verified each fold by file and line. It confirmed thatregistry_version > 0matches the platform (a deployment vocabulary carries 2, a test fixture 0), and that the copied sources build the package (a static version, nosetup.cfgorMANIFEST.in). Of its seven low items, six were folded atbdbb691:sys.path, as the examples do;PYTHONPATHis unset once for the whole script;Not folded: process wording in one commit message, which this repository's squash merge does not carry.
CI notes
All four required contexts are green by name at
7398589:lint(run 34810048319),Lint — no mocks in runtime-e2e/(run 34810048184), Runtime E2E required for user-facing changes (run 34810048184) and Test Summary (run 34810048319). Every other check at the head passed (CodeQL, DCO, build, test (3.11), Contract Tests (Python 3.11), Demo Scripts Validation, Validate Version Alignment, wire-shape-contract, real-stack ubuntu-latest), and Integration Tests (Community Stack) reported skipped.