Skip to content

feat(typed-policies): typed clients for the six authoring routes - #255

Merged
saurabhjain1592 merged 1 commit into
mainfrom
feat/3746-typed-policies
Sep 13, 2026
Merged

saurabhjain1592 merged 1 commit into
mainfrom
feat/3746-typed-policies

Conversation

@saurabhjain1592

Copy link
Copy Markdown
Member

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-policies for that, and this SDK had no client for any of them. This PR adds client.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): TypedPoliciesNamespace with six methods.
    • edition(), validate(document, fixtures), publish(document, fixtures), activate(digest, *, reason=None), active() and system().
    • The three models that share a spec schema name (EditionConstructReport, AuthoringFinding and TypedAuthoringDocumentRequest) are registered by the wire-shape contract and match the spec exactly. The results are TypedAuthoringEdition, TypedPolicyValidation, TypedPolicyPublication, TypedPolicyActivation, ActiveTypedPolicy and TypedPolicySystemCorpus.
    • document stays 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 as source, beside the parsed document, and None when nothing is active.
    • Null-tolerant collections. The platform marshals a nil Go slice or map as JSON null; a real stack answers a clean validation with "findings": null. The seven collections it declares without omitempty read null as empty.
  • TypedPolicyRefusal (axonflow/exceptions.py) carries status, reason, code, findings and retry_after for every refusal. A 401 stays an AuthenticationError.
    • A document the save-time checks reject is 422 document_refused, carrying its findings.
    • A publication refused later, such as one declaring no fixtures, is 422 publication_refused and names its cause in the message.
  • Scope. 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.
  • The client. typed_policies is on both the async and the sync client, built lazily and once. A client derived with as_user() gets its own namespace, bound to itself.
  • Exports in 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

  • The six routes' request and response schemas: the spec at platform commit 36e0e96b7e5c16626d394272b727b533f2b94a04, the commit this SDK's wire-shape snapshot pins. The specs are unchanged from there to 857455033.
  • tests/fixtures/typed_policy_publish_body.json: the body the platform's own route test proves publishable, marshalled by the platform's own types at 857455033.
  • Which collections can arrive as null: read from the platform's types at 857455033.

Testing

  • Unit: tests/test_typed_policies.py, 28 tests. They cover:

    • each operation on the wire (method, route and exact body) and on its answer;
    • every documented refusal: 400, 402 with and without Retry-After, 404, 409, 422 with and without findings, 429, 503, the 401 mapping, and a refusal with no JSON body;
    • the platform's real null shapes;
    • the sync client, and a derived client's namespace.
  • Mutation: 16 mutants, each breaking one guard, each killed by its named test:

    • the seven null-tolerant fields, one at a time;
    • the 401 mapping, Retry-After, the refusal's findings, and active()'s 404;
    • the activation reason, the omitted fixtures member, and the exact signed bytes;
    • the sync twin, and the derived client's namespace.

    Every mutant was compiled from source, with no bytecode cache, and restored in a finally.

  • Real stack: runtime-e2e/typed_policies/test.py against the platform at 857455033 in community mode, on the application database role. 16 of 16 assertions passed:

    • nothing is active at first, so active() is None;
    • edition() reports root organization, max_documents 20 and persistence database; system() returns 118 controls with digest sha256:8ceb18b111d56e14…;
    • the fixture document validates clean, publishes (sha256:d7c3b9a011b87efb…, version 1) and is promoted;
    • active() returns it as the exact signed source, with the author the platform stamped (the runtime-e2e client), not the someone-else the document names;
    • re-activating it is 409 activation_refused;
    • publishing with no fixtures is 422 publication_refused: "the publication declares no fixtures, so no policy in this document has been shown to do anything";
    • for a document naming an unregistered action, validate() reports ACTION_NOT_REGISTERED (reject) on grant.refund, and publish() is 422 document_refused carrying that finding.

    The first runs found two defects the mocks could not: the platform's null collections, 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.

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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant