feat(typed-policies): typed clients for the six authoring routes - #255
Merged
Merged
Conversation
A v11 platform authors policy as a typed document: validated, published as a signed artifact pinned by its digest, and promoted to active. The agent proxies six routes under /api/v1/typed-policies for that, and the SDK had no client for any of them. client.typed_policies (and the synchronous client's twin) adds edition(), validate(), publish(), activate(), active() and system(). The request and result models follow the spec's schemas, and the three that share a spec name (EditionConstructReport, AuthoringFinding, TypedAuthoringDocumentRequest) are held to it by the wire-shape contract. The document itself stays an opaque object, as the spec defines it, so a field the policy vocabulary gains passes through unchanged rather than being dropped by a client-side model. The platform marshals a nil Go slice or map as JSON null: a real stack answers a clean validation with "findings": null. Every collection the platform declares without omitempty therefore reads null as empty. Every refusal raises TypedPolicyRefusal with the HTTP status, the platform's reason, any findings and Retry-After; a 401 stays an AuthenticationError. A document the save-time checks reject is a 422 document_refused carrying its findings; a publication refused later, such as one declaring no fixtures, is a 422 publication_refused that names its cause in the message. active() answers None when nothing is active. activate() promotes a published digest. Rollback and withdraw are customer portal operations the agent does not proxy, so the SDK has no method for either, and the README and the module say so. Signed-off-by: Saurabh Jain <saurabh.jain@getaxonflow.com>
This was referenced Sep 13, 2026
Merged
Merged
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
A v11 platform authors policy as a typed document: validated, published as a signed artifact pinned by its digest, and promoted to active. The agent proxies six routes under
/api/v1/typed-policiesfor that, and this SDK had no client for any of them. This PR addsclient.typed_policies, typed end to end, and proves it against a real stack. It is the Python half of the typed-policy step of the v11 SDK train; the TypeScript SDK follows with the same shape.Changes
axonflow/typed_policies.py(new):TypedPoliciesNamespacewith six methods.edition(),validate(document, fixtures),publish(document, fixtures),activate(digest, *, reason=None),active()andsystem().EditionConstructReport,AuthoringFindingandTypedAuthoringDocumentRequest) are registered by the wire-shape contract and match the spec exactly. The results areTypedAuthoringEdition,TypedPolicyValidation,TypedPolicyPublication,TypedPolicyActivation,ActiveTypedPolicyandTypedPolicySystemCorpus.documentstays a plain JSON object, as the spec defines it: "the authoring model itself rather than a mirror of it, so a field added to the policy vocabulary is authorable here without this schema changing". A client-side model would drop the fields the vocabulary gains.active()returns the exact signed bytes assource, beside the parseddocument, andNonewhen nothing is active.null; a real stack answers a clean validation with"findings": null. The seven collections it declares withoutomitemptyreadnullas empty.TypedPolicyRefusal(axonflow/exceptions.py) carriesstatus,reason,code,findingsandretry_afterfor every refusal. A 401 stays anAuthenticationError.422 document_refused, carrying its findings.422 publication_refusedand names its cause in the message.activate()promotes a published digest, and a digest whose version does not advance is refused. Rolling back and withdrawing are customer portal operations the agent does not proxy, so the SDK has no method for either; the module docstring and the README say so.typed_policiesis on both the async and the sync client, built lazily and once. A client derived withas_user()gets its own namespace, bound to itself.axonflow/__init__.py, a README section, and a CHANGELOG[Unreleased]entry..lint_baselines/falsey_clobber.json. The client's added lines shift the line numbers of 18 existing entries. They are the same findings: the new and stale entries' source text is equal one to one, and none falls on an added line.The platform this models
36e0e96b7e5c16626d394272b727b533f2b94a04, the commit this SDK's wire-shape snapshot pins. The specs are unchanged from there to857455033.tests/fixtures/typed_policy_publish_body.json: the body the platform's own route test proves publishable, marshalled by the platform's own types at857455033.null: read from the platform's types at857455033.Testing
Unit:
tests/test_typed_policies.py, 28 tests. They cover:Retry-After, 404, 409, 422 with and without findings, 429, 503, the 401 mapping, and a refusal with no JSON body;nullshapes;Mutation: 16 mutants, each breaking one guard, each killed by its named test:
Retry-After, the refusal's findings, andactive()'s 404;fixturesmember, and the exact signed bytes;Every mutant was compiled from source, with no bytecode cache, and restored in a
finally.Real stack:
runtime-e2e/typed_policies/test.pyagainst the platform at857455033in community mode, on the application database role. 16 of 16 assertions passed:active()isNone;edition()reports rootorganization,max_documents20 and persistencedatabase;system()returns 118 controls with digestsha256:8ceb18b111d56e14…;sha256:d7c3b9a011b87efb…, version 1) and is promoted;active()returns it as the exact signed source, with the author the platform stamped (theruntime-e2eclient), not thesomeone-elsethe document names;409 activation_refused;422 publication_refused: "the publication declares no fixtures, so no policy in this document has been shown to do anything";validate()reportsACTION_NOT_REGISTERED(reject) ongrant.refund, andpublish()is422 document_refusedcarrying that finding.The first runs found two defects the mocks could not: the platform's
nullcollections, and my assumption that the no-fixtures refusal carries findings. Both are fixed above, and the mocks now carry the platform's real shapes.The local round: every step of every pull-request workflow that runs without a stack. That is ruff check and format, mypy, the falsey-clobber lint, the AuthZEN regeneration check, the no-mocks lint, version alignment, the wire-shape contract against the snapshot, the contract tests, the examples' compile and import checks, the full suite (1576 passed, 30 skipped, 85.38% coverage), the build, and
twine check.Integration tests are unchanged. The integration job runs against the community mirror's main, which is the v10 platform until the v11.0.0 tag and has no typed-policy routes, so this PR's runtime proof is the local stack above.