Skip to content

Features batch 3: bulk-write audit, worker images, server head, SPA mounts, resource access (#557 #540 #535 #541 #526) - #574

Merged
antosubash merged 32 commits into
mainfrom
feat/features-batch-3
Oct 22, 2026
Merged

antosubash merged 32 commits into
mainfrom
feat/features-batch-3

Conversation

@antosubash

Copy link
Copy Markdown
Owner

Features batch 3: bulk-write audit, worker images, server-rendered head (step 1), SPA mounts, and resource-scoped access with a new sharing module. Design: docs/superpowers/specs/2026-10-21-features-batch-3-design.md.

What changes

  • audit: bulk/Core/raw DML writes are never audited #557 Bulk, Core and raw DML is now audited.
    • New simple_module_db.bulk_audit. For UPDATE or DELETE on an audited table, it prefetches the affected ids (same WHERE, same params, same tenant scope) and writes one bulk_updated / bulk_deleted / bulk_soft_deleted / bulk_restored row per id, up to a cap. Past the cap it writes one summary row.
    • INSERTs are always audited.
    • Opt out per statement with execution_options(sm_audit=False).
    • Raw text() writes go through record_bulk_audit().
    • New audit_log DB setting refuse_unaudited_dml.
  • cli/docker: scaffold separately built worker images (native libs / GPU) alongside the web image #540 Separate worker images. smpy docker add-worker <name> --queues --base --apt --extras --modules writes a Python-only worker Dockerfile and a compose override, and includes the override from the compose file. Every input is strictly validated, and re-running is idempotent.
  • inertia: server-side rendering (or server-rendered <head>) for public pages #535 step 1: server-rendered head.
    • simple_module_inertia.Head (title, description, canonical, robots, og, twitter, JSON-LD) can be passed to inertia.render(..., head=...) and is rendered into the HTML <head>, escaped.
    • Canonical and og:url links are built from the new HostSettings.public_base_url, never from the request's Host header.
    • inertia: server-side rendering (or server-rendered <head>) for public pages #535 stays open for step 2, a Node SSR sidecar.
  • hosting/ui: mount a legacy client-routed SPA under a prefix (deep-link fallback + scoped link interception) #541 Legacy SPA mounts.
    • New ModuleBase.spa_mounts() returning SpaMount(prefix, dist_dir, auth=...).
    • The mount serves index.html as the deep-link fallback for HTML requests, returns a 404 for a missing asset (never HTML), and marks hashed assets immutable.
    • Required-auth mounts are served private.
    • New diagnostic SM032: a mount prefix overlapping a route fails the boot.
    • The nativePrefixes shared prop and HostSettings.native_link_prefixes stop Inertia from intercepting links into the mount.
  • permissions: resource-scoped access (owner / shared-with-user / public) and permission-aware list filtering #526 Resource-scoped access.
    • New ResourcePolicy (can/filter, kept in parity) and a ResourceTypeRegistry filled by register_resource_types.
    • New RequiresResource dependency: an invisible resource returns 404, one visible but not allowed returns 403.
    • New OwnedResourcePolicy helper (owner, public visibility, shares).
    • Optional sharing module: table sharing_resource_share, migration f733c7938fd1, label sharing. Grants are validated against the registry and the owning module's can("share"), and tenant membership is checked through the core tenant_member_ids seam. Admin page included.

The migration graph test pins 4 roots and 6 heads.

Verification (after merging main with batch 2)

  • make lint ✓
  • make test-py: 5566 passed ✓
  • make test-js: 685 ✓
  • make ci-doctor ✓
  • uv lock --check ✓
  • Redis-backed tests: 183 ✓

/ship

  • Optimize pass applied.
  • Review: Claude plus an independent Codex review. Clean after 2 passes. Pass 1 fixed a P1: the bulk-audit prefetch dropped bound WHERE params.
  • Background security reviews fixed bulk-audit coverage, the SPA public-exemption scope, resource-policy can/filter parity, and cross-tenant enumeration in sharing.
  • QA: clean, 0 failures and no console errors. Two P3 gaps:
    • No shipped route uses Head() yet.
    • No module registers a resource type, so sharing grants are covered by unit tests only.
  • Ship report: https://claude.ai/artifact/Joh4mGRqEUfwhjPPMftSRX

Manual: simple_module_sharing is a new PyPI package, so it needs a pending publisher registered before release.

Closes #557
Closes #540
Closes #541
Closes #526

https://claude.ai/code/session_01M9neheZZEe3sVpDi2S3zT4

Head DTO + inertia.render(head=), escaped tags with data-inertia keys, _head prop re-emitted via Inertia serverHead, public_base_url host setting (never the request Host), both index.html templates, docs, tests.

Claude-Session: https://claude.ai/code/session_01M9neheZZEe3sVpDi2S3zT4
…ResourcePolicy, RequiresResource (#526)

ResourcePolicy protocol (filter + can), ResourceTypeRegistry and the
register_resource_types hook (app.state.sm.resource_types);
OwnedResourcePolicy builds owner OR public(view) OR EXISTS(share) with an
explicit bypass permission, so lists and paginate() counts are filtered in
SQL; RequiresResource loads only through policy.filter: invisible -> 404,
visible but not allowed -> 403, and is a token gate only with permission=.

Claude-Session: https://claude.ai/code/session_01M9neheZZEe3sVpDi2S3zT4
do_orm_execute listener (simple_module_db.bulk_audit) records ORM bulk and
Core INSERT/UPDATE/DELETE on audited models through the per-engine audit
sink: ids prefetched with the statement's WHERE and tenant scope, per-row
entries up to 500, else one '*' summary. sm_audit=False opts out;
record_bulk_audit() for text() writes; audit_log setting
refuse_unaudited_dml makes the opt-out raise.

Claude-Session: https://claude.ai/code/session_01M9neheZZEe3sVpDi2S3zT4
…ception (#541)

SpaMount + ModuleBase.spa_mounts(), SM031 overlap diagnostic, nativePrefixes
shared prop, native_link_prefixes host setting, startSpaLinkInterception({ exclude }).

Claude-Session: https://claude.ai/code/session_01M9neheZZEe3sVpDi2S3zT4
# Conflicts:
#	CHANGELOG.md
#	host/client_app/app.tsx
…es (#526)

can() lower-cased both sides while the SQL predicate compared the coerced
id; on a string owner column a case-variant id passed one and not the
other. Both now go through the same _coerce + ==.

Claude-Session: https://claude.ai/code/session_01M9neheZZEe3sVpDi2S3zT4
…e recorded (#557)

Changes come from the statement's SET/VALUES targets resolved to mapped
attributes (bound values through the execute params, column-named params as
SQLAlchemy writes them); stray keys and excluded columns under Core keys no
longer leak. is_deleted written as SQL, a truthy value or in executemany rows
is bulk_soft_deleted, a falsy one bulk_restored; PK and soft-delete-flag
writes are never skipped as bookkeeping, so strict mode refuses them.

Claude-Session: https://claude.ai/code/session_01M9neheZZEe3sVpDi2S3zT4
…atement values (#557)

A statement writing is_deleted keeps its other columns in changes; executemany
rows get soft-delete/restore per row (distinct summaries past the cap). Each
executemany row is the statement's .values() resolved through that row plus
its column-named params, so a write cannot look excluded-only and escape
auditing or strict mode. Executemany no longer prefetches with unbound params.

Claude-Session: https://claude.ai/code/session_01M9neheZZEe3sVpDi2S3zT4
…ss (#526)

sharing_resource_share (tenant-scoped, audited, unique incl. tenant_id,
hard delete on revoke) with share_exists_for() for OwnedResourcePolicy;
expiry is checked in SQL. Grant/revoke/list API validates the resource
type against the registry, the granter's can("share") on the row and the
grantee via the user directory. Admin page /admin/sharing (en + de).
Migration f733c7938fd1: a new root and head labelled sharing.

Claude-Session: https://claude.ai/code/session_01M9neheZZEe3sVpDi2S3zT4
…ing only for UPDATE (#557)

An INSERT setting only excluded columns or is_deleted was dropped as
excluded-only bookkeeping, and so also escaped refuse_unaudited_dml. Inserts
now always produce at least the summary row.

Claude-Session: https://claude.ai/code/session_01M9neheZZEe3sVpDi2S3zT4
…526)

On a multi-tenant app grant() now requires the grantee to be a member of
the active tenant and answers a non-member with the same 422 as an
unknown id (no cross-tenant enumeration); share listings only carry
directory labels for members. Membership is read through a new core seam,
simple_module_core.tenancy.tenant_member_ids (app.state.tenant_members,
published by tenants, using the caller's session); with no active tenant
or no membership store nobody qualifies. Single-tenant behaviour is
unchanged.

Claude-Session: https://claude.ai/code/session_01M9neheZZEe3sVpDi2S3zT4
Resolve conflicts keeping both sides: hosting batch 1 (CORS, forwarded
host, rate-limit registry, DB options, OpenAPI security/SM031, request
hooks mixin, RequestRegistries) alongside batch 3 (resource types, SPA
mounts/SM032, server-rendered head, bulk audit, docker add-worker).
register_resource_types stays on ModuleBase (not a request hook) and is
passed to run_module_registrations next to RequestRegistries. Split
_default_api_version into _api_version.py to keep host_settings.py under
the 300-line cap.

Claude-Session: https://claude.ai/code/session_01M9neheZZEe3sVpDi2S3zT4
# Conflicts:
#	CHANGELOG.md
#	CLAUDE.md
#	docs/database/migrations.md
#	host/tests/test_migration_graph.py
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 22, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
🔒 Security Review ✅ Completed 2026-10-22T09:52:02.097922Z 80e5701 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Oct 22, 2026 •

Copy link
Copy Markdown

Deploying simple-module-python with  Cloudflare Pages  Cloudflare Pages

Latest commit: b5868a6
Status: ✅  Deploy successful!
Preview URL: https://e9dbc8de.simple-module-python.pages.dev
Branch Preview URL: https://feat-features-batch-3.simple-module-python.pages.dev

View logs

CI forces Rich colour, so '--force' in the rendered usage error was split by
escape codes. Run the CLI wide and colourless and normalise the output.

Claude-Session: https://claude.ai/code/session_01M9neheZZEe3sVpDi2S3zT4
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment