Skip to content

SpaceWorks 0.7.5 - #15

Merged
Shaan-Shoukath merged 398 commits into
mainfrom
dev
Sep 1, 2026
Merged

SpaceWorks 0.7.5#15
Shaan-Shoukath merged 398 commits into
mainfrom
dev

Conversation

@Shaan-Shoukath

@Shaan-Shoukath Shaan-Shoukath commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator

Merges 66 commits from dev. Merging this publishes a releaserelease.yml fires on main, and VERSION changed, so it will build and push spaceworks-backend / spaceworks-frontend images to ghcr and publish a GitHub Release titled SpaceWorks 0.7.5.

What's in it

Events program (four phases) — cross-makerspace collaborative events with host-waiver acceptance, QR check-in, opt-in attended-events on the maker profile, and registration decoupled from presence (presence is now proven at the door by the staff-scanned QR rather than a self-declared session).

Module-architecture program — operator-facing module groups, one module map per staff tab, payments/accounts/mobile/updates keys, Razorpay behind a provider seam, a superadmin modules console, cloud/full deploy profiles, the account-less identity seam with staff-created walk-in members, four platform login-method switches, maker profiles and a member directory, and staff-side event registration.

Notifications v2 — per-event recipient selection, per-room chat destinations, editable email/chat wording, and object-scoped recipient rules.

Plus scoped PII encryption, custom per-makerspace roles, machine-scoped MANAGE_MACHINES, and an accessibility floor (contrast guard, focus indicators, skip links).

Release mechanics

  • VERSION 0.5.1 → 0.7.5
  • The OpenAPI document version now tracks VERSION — it had been pinned at 0.1.0 while VERSION climbed to 0.5.1, so every schema consumer was told a version that had never been true. A test now fails if the two drift.
  • Release titles are SpaceWorks <version> instead of v<version>. The immutable tag keeps its v<base>-main.<run>.<sha> form.
  • CHANGELOG.md gains a 0.7.5 section. Note 0.3.0–0.5.1 were never recorded — generation stopped after 0.2.0 — so that window points at git log instead of inventing entries.

Review

The final commit closes four rounds of codex exec review on the events program. Each round found a defect the previous one missed, all the same shape — money or access in a state transition happening later than the code assumed:

  1. A collaborator purging events stranded host-raised charges: a receipt vanished and a pending charge became unpayable.
  2. The collaborative registration route bypassed the configured rate limit; the naive fix would have 429'd the only path a member has to repair a missing waiver acceptance.
  3. A cancelled registration is reactivated rather than treated as a duplicate, so counting it as a "retry" reopened the bypass on a larger budget.
  4. Check-in reported waiver evidence from only one of the two places it can live, so it was structurally false for every host member.

Round 4 returned no actionable defects.

Verification

Backend suite green against the seven known pre-existing failures, each verified by reason rather than name. Tombstone suite 140, vitest 76, tsc -b clean, migration drift clean. New regression tests were each confirmed to fail when the bug they guard is reintroduced.

QA

End-to-end QA signed off by the repository owner (@Shaan-Shoukath). Recorded here as the owner's attestation — it was not performed or observed by the automated review, which covered the test suites, static checks and the build only.

Automated verification for the record: backend suite green against the seven known pre-existing failures (each confirmed by failure reason, not name), tombstone suite 140, vitest 76, tsc -b clean, migration drift clean. Each new regression test was confirmed to fail when the bug it guards is reintroduced.

Known open item — follow-up in progress

A host purging events still deletes visiting members' charges: module_purge._purge removes every host-owned event payment, including one routed to a visitor's own space, so a paid receipt is destroyed and a pending debt silently vanishes.

This predates the PR and is being fixed separately on fix/purge-preserves-visiting-member-payments — no module purge will delete a payment at all, and a subject_label snapshot on Payment keeps a surviving receipt readable.

Correction to an earlier version of this note: it claimed a subject-less pending charge would still be payable. That is false. Payment.save() calls full_clean() unconditionally and Payment.clean() requires the subject row to exist, so every write path — hosted checkout, mobile intent, offline reconciliation, and webhook settlement — fails once the subject is gone. Read paths degrade safely; write paths do not. Making clean() tolerate a missing subject on an otherwise-unchanged existing payment is therefore part of the fix, not an optimisation.

🤖 Generated with Claude Code

Shaan-Shoukath and others added 30 commits August 18, 2026 00:57
…rt and cutover

Co-Authored-By: Shaan-Shoukath <shaanshoukath4522@gmail.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Codex GPT-5.6 <noreply@openai.com>
Co-Authored-By: Shaan-Shoukath <shaanshoukath4522@gmail.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…apps

Co-Authored-By: Shaan-Shoukath <shaanshoukath4522@gmail.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…e, import and console

Co-Authored-By: Shaan-Shoukath <shaanshoukath4522@gmail.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Codex GPT-5.6 <noreply@openai.com>
…ry, and per-tenant fan-out isolation

Co-Authored-By: Shaan-Shoukath <shaanshoukath4522@gmail.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Codex GPT-5.6 <noreply@openai.com>
…sons, and audit inside the completion transaction

Co-Authored-By: Shaan-Shoukath <shaanshoukath4522@gmail.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Codex GPT-5.6 <noreply@openai.com>
… recovery failures

Co-Authored-By: Shaan-Shoukath <shaanshoukath4522@gmail.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Codex GPT-5.6 <noreply@openai.com>
…d split failure cleanup out

Co-Authored-By: Shaan-Shoukath <shaanshoukath4522@gmail.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Codex GPT-5.6 <noreply@openai.com>
…bandoning the lease

Co-Authored-By: Shaan-Shoukath <shaanshoukath4522@gmail.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ry-file size limit

CLAUDE.md was 227k chars against a threshold of max(40000, ctx*0.05*chars_per_token)
— 150k on a 1M-context model, 40k on a 200k one — so it warned on every session.
Invariants, the source map and the condensed changelog move to docs/, verbatim;
what stays inline is what an agent must obey without knowing to look it up.
Verified zero of 718 identifiers lost across the split.

Co-Authored-By: Shaan-Shoukath <shaanshoukath4522@gmail.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
ChangePasswordGate had three password inputs with no autoComplete and no username
field, so Chrome/Firefox/Safari could not offer to update a saved credential. Adds
current-password/new-password, a visually-hidden sr-only username field (not
display:none, which managers ignore), and label/input association. The Add-staff
modal marks its password new-password so the actor's own credential is not autofilled
into a field that sets someone else's.

Co-Authored-By: Shaan-Shoukath <shaanshoukath4522@gmail.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Codex GPT-5.6 <noreply@openai.com>
…ve, and close the ledger fail-open

age recipient encryption authenticates nothing about the sender, so an optional
expected_sha256 meant a planted archive was hashed by us and that hash recorded as
authoritative. The digest is now required and validated before any upload or row write,
and verify_content_ledger refuses an empty ledger for v2 archives, rejects duplicate
paths, and asserts the ledger covers every payload file (manifest.json excepted).

Co-Authored-By: Shaan-Shoukath <shaanshoukath4522@gmail.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Codex GPT-5.6 <noreply@openai.com>
… silently re-enabling them

Both capability reads swallowed every exception and returned permissive defaults, so a
transient database fault re-enabled login methods an operator had turned off. The reads
still must never lock anyone out, so they now write through a cache of the four booleans
on success, return the cached values on failure, and fall back to permissive defaults only
when nothing was ever cached. Failures are logged at ERROR; the read path still performs
no database write.

Co-Authored-By: Shaan-Shoukath <shaanshoukath4522@gmail.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Codex GPT-5.6 <noreply@openai.com>
…e ledger on a late payment

_expire_checkout_best_effort only looked for a checkout session, so a native PaymentSheet
charge (which sets only stripe_payment_intent_id) stayed live after staff waived or marked
it paid offline, and the member could still pay a charge the ledger called waived. The
closer now cancels the intent too, still best-effort so reconciliation can never be blocked
and strand a charge. A paid webhook arriving after WAIVED now promotes the row to
PAID_ONLINE through a narrow transaction-scoped trigger exception; after PAID_OFFLINE it
preserves the prior resolution and records payment.double_paid_refund_required.

Co-Authored-By: Shaan-Shoukath <shaanshoukath4522@gmail.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Codex GPT-5.6 <noreply@openai.com>
…ckout feature gate, and gate printer stats

The three member payment routes were mounted unconditionally in the makerspaces urlconf,
so a deployment with payments tombstoned kept resolving them, kept them in the OpenAPI
schema, and could still create real provider checkouts after reconciliation and the webhook
were gone. They move to apps/payments/urls_member.py spliced in by config.urls.separable,
with paths and route names unchanged. checkout_tool now re-checks inventory.self_checkout
under the makerspace row lock, closing the window between the view's check and the issue;
return_tool deliberately keeps no locked guard so an admitted return cannot be stranded
mid-toggle. Public stats now require both machine_service and printing before publishing
printer telemetry.

Co-Authored-By: Shaan-Shoukath <shaanshoukath4522@gmail.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Codex GPT-5.6 <noreply@openai.com>
…e does not apply

A superadmin holding an explicit machine-scoped membership in a makerspace that disabled
superadmin access still answers ALL at the action level, so the 'if manage_scope is
rbac.ALL: return queryset' shortcut handed them every machine and their role's links did
nothing. Four sites now normalise the sentinel to the tenant ids the queryset represents
and let role_scope's plural resolver decide EXEMPT versus role-linked per makerspace, so an
ordinary superadmin is unaffected while a hidden-space membership is scoped. Covers the
machine querysets, service files and consumable pools.

Co-Authored-By: Shaan-Shoukath <shaanshoukath4522@gmail.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Codex GPT-5.6 <noreply@openai.com>
… and throttle presence starts

NUM_PROXIES defaulted to None, so DRF keyed throttles on the whole caller-supplied
X-Forwarded-For and an attacker varied it for unlimited buckets. Defaulting to 0 instead
would collapse every user behind an unconfigured proxy into one bucket, so there is no safe
blind default: production now refuses to boot when DEBUG is False and TRUSTED_PROXY_COUNT is
unset, development defaults to 0, and django-axes derives its proxy count from the same
value so the throttle and the lockout cannot disagree about who the client is.
PresenceStartView gains a per-member throttle; identical-duration replays stay idempotent
and do not consume it.

Co-Authored-By: Shaan-Shoukath <shaanshoukath4522@gmail.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Codex GPT-5.6 <noreply@openai.com>
…staged files to their owner, refuse a bucket collision

AuditLog is append-only, so raw invitation emails and claim redemption IPs written into
meta were permanently readable by any VIEW_AUDIT holder. record() now sanitises metadata
centrally by value shape and key token, replacing them with domain-separated HMAC
fingerprints while leaving key names and the tenant_migration audit-reference paths intact.
finalize_file now requires owner_user_id=actor.pk before storage promotion and revalidates
under the row lock, so staff can no longer attach and read another user's staged CAD file.
And equal PUBLIC_IMAGE_BUCKET/AWS_STORAGE_BUCKET_NAME now fails at compose init, as a Django
system check, and in settings validation, since a check alone does not run for a direct
gunicorn or Celery start.

Co-Authored-By: Shaan-Shoukath <shaanshoukath4522@gmail.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Codex GPT-5.6 <noreply@openai.com>
…ze refresh rotation

RecoveryStateView was IsAuthenticated only, so any member could read deployment recovery
state: auth generation, restore ids, the recovery principal and quarantine acknowledgements.
It now admits only an active superadmin or the designated recovery principal, reusing
recovery.py's own notion of the principal, and the quarantine route allowlist is unchanged so
the principal can still acknowledge while quarantined. Browser and claim refresh rotation
also delegated to SimpleJWT without serializing the blacklist check against the replacement
mint, so a copied refresh token submitted concurrently yielded two independently rotating
descendants; both paths now share one service that locks OutstandingToken by JTI inside a
transaction and mints exactly one descendant, the loser getting the existing token_not_valid.

Co-Authored-By: Shaan-Shoukath <shaanshoukath4522@gmail.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Codex GPT-5.6 <noreply@openai.com>
…cy-overriding subclass

P5a's PresenceStartThrottle overrode allow_request and get_cache_key, which the
claim-route guard refuses outright: an allow_request override can silently disable
throttling on a claim-reachable route. It also resolved the makerspace inside
allow_request via get_public_makerspace, whose Http404 would have rewritten the response
from a throttle -- the same anti-pattern the source-gate invariant records. The view
already declared throttle_scope, so MemberPrincipalRateThrottle covers it directly.
Accepted tradeoff: identical-duration replays now consume the budget; the generous
production rate is what protects legitimate re-checkins.

Co-Authored-By: Shaan-Shoukath <shaanshoukath4522@gmail.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…revoked members reading machine data

A Space Manager could point a Slack/Mattermost/Discord webhook at loopback, link-local or
private space and have the backend POST from inside the platform network; urlopen also
followed redirects. Every DNS answer is now checked and unsafe addresses rejected, at save
time and again immediately before delivery, and delivery connects a socket to the validated
sockaddr so the hostname is never re-resolved -- closing DNS rebinding, which a
resolve-then-urlopen check cannot. TLS keeps the original hostname for SNI and certificate
verification and sends it as Host; redirects are followed manually, capped at three hops,
each target revalidated and pinned. No vendor allowlist, so self-hosted Mattermost still
works. Separately, stale MachineOperator rows no longer grant a revoked member private
machine reads in either resolver, and operator candidates and service-request requesters now
require an active membership and active access status.

Co-Authored-By: Shaan-Shoukath <shaanshoukath4522@gmail.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Codex GPT-5.6 <noreply@openai.com>
Shaan-Shoukath and others added 28 commits August 25, 2026 10:23
The Lane D spec requires operator documentation to state that core dumps, process
dumps and swap sit outside the application-level DEK guarantee. The sentence named
process dumps and swap but never core dumps, so test_dek_guarantee_d8 was a standing
red on dev - the test shipped without the documentation half it asserts.

Co-Authored-By: Shaan-Shoukath <shaanshoukath4522@gmail.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
scripts/import-backup.sh ends with exec "$ROOT/scripts/restore.sh", but restore.sh
was committed 100644 and nothing chmods it: install.sh unpacks the release tarball
with git's modes and only chmods install.sh and spaceworks-compose.sh. A backup
import therefore died with Permission denied at the handoff, after it had already
recorded the restore intent and released its lock. Same defect class as the
spaceworks-compose.sh fix in 6c17f3d, so this adds the guard that was missing:
the privileged shell scripts must be executable, and whatever a shipped script
execs must be executable too.

Co-Authored-By: Shaan-Shoukath <shaanshoukath4522@gmail.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The test built its fake deployment against an older ops layout: it wrote the age
identity to ops/age-identity.txt and shipped no compose wrapper, so import-backup.sh
died at its second precondition with exit 1 while the test asserted 23. It was a
standing red that never reached the behaviour it names. setup.sh and the script both
use ops/work/age-identity.txt, so the test now builds that, plus a compose-wrapper
stub that forwards to the existing docker stub.

Co-Authored-By: Shaan-Shoukath <shaanshoukath4522@gmail.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
_coverage_failures built its result in a defaultdict and called
failures[makerspace_id].extend(row_errors) unconditionally, so a makerspace with no
errors still created an empty list entry. dict(failures) was therefore truthy for a
perfectly covered run and finalize_run marked every successful run FAILED. Also
re-points two K2 fixtures at the current schema: Makerspace.slug is varchar(50) and
the generated slugs reached 52 characters, and makerspace-scoped archives must carry
superadmin_access_at_decision for the backup_makerspace_decision_snapshot_present
constraint that landed after this work was written.

Co-Authored-By: Shaan-Shoukath <shaanshoukath4522@gmail.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Codex GPT-5.6 <noreply@openai.com>
…K2 models

The K2 merge added backup.BackupRun and backup.BackupRunCoverage and gave
BackupArchive backup_run, build_holder and staging_object_key, which invalidates the
checked-in schema/catalog digest and failed 26 tests across tenant_migration. Blessing
the new digest is the explicit review act the constant's comment asks for: the graph
diff against the pre-merge tree contains exactly those two models and those three
fields and nothing else.

Co-Authored-By: Shaan-Shoukath <shaanshoukath4522@gmail.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ion handler

The pooler-safe gate took its advisory lock on connections["default"].copy(alias=...),
an alias absent from settings.DATABASES. django.contrib.postgres connects
register_type_handlers to connection_created, which resolves connections[alias] to read
hstore OIDs, so every lock connect raised ConnectionDoesNotExist and every gated write
boundary answered 503 - in production as well as tests. Registering the alias cleared
that but ran into pytest-django refusing undeclared aliases, and left a spurious extra
connection per process purely for the OID lookup.

This connection is only ever a transaction-scoped advisory-lock holder, so it does not
belong in the ORM's connection handler at all: it is now a raw psycopg2 connection built
from the default backend's own get_connection_params(), which already carries the
effective database name under pytest. Backend-PID continuity, the granted pg_locks
check, rollback-then-close release, failure conversion to SourceMigrationGateUnavailable
and an independent connection per nested boundary are all preserved.

Co-Authored-By: Shaan-Shoukath <shaanshoukath4522@gmail.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Codex GPT-5.6 <noreply@openai.com>
…ours

Resolves the five-week-old filament-wip work onto the current tree. Two conflicts:
models_service.py had become a thin re-export barrel under an LOC split, so the pool's
new machine_type FK, color_hex, is_public and its two check constraints move into
models_service_consumables.py where MachineConsumablePool now lives. public_status kept
both sides' intent: machine-type identity comes from resolve_global_printer_type() as
the branch intended (its PRINTER_SLUG import is gone), while tenancy stays routed
through the servability policy rather than the branch's raw archived_at filter.

Co-Authored-By: Shaan-Shoukath <shaanshoukath4522@gmail.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…stries

filament-wip predates the field-disposition guards, so its three new
MachineConsumablePool fields had no dispositions: the Lane D field catalog and the
data_export REDACTED list both refused the build. machine_type is a structural FK,
color_hex a hex string and is_public a visibility flag - none is PII - so they are
carried exactly like their siblings machine, color and is_active. Also recomputes the
Lane D schema digest, whose only drift against the post-K2 tree is these three fields.

Co-Authored-By: Shaan-Shoukath <shaanshoukath4522@gmail.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The checked-in snapshot predated 0022_target_import_recovery_mode, so
RecoveryStateModeEnum was missing target_import in both the schema and the generated
client. Regenerating after the filament merge picks that up; spectacular writes no
trailing newline, so it is restored to keep the file's diff honest.

Co-Authored-By: Shaan-Shoukath <shaanshoukath4522@gmail.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…jection policy

The filament merge added MachineConsumablePool.machine_type, and every FK pointing at
machines.MachineType needs a declared projection policy or the registry guard refuses the
build. It remaps through the built-in/custom machine type map, exactly like the machine,
pricing and service-queue edges: projecting a tenant into a target deployment must land
its pools on the target's equivalent type rather than a source primary key. This is the
fourth registry the three new fields touched, after the Lane D field catalog, the
data_export dispositions and the schema digest.

Co-Authored-By: Shaan-Shoukath <shaanshoukath4522@gmail.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…sibility

The filament merge changed what the machines module offers, and MODULES.md is the
user-facing page the README links module names into. Records the either-machine-or-type
scoping and states exactly what a public pool exposes: PublicPrinterPoolSerializer emits
id, material and colour name only, so the hex swatch, lot code and remaining grams stay
staff-only.

Co-Authored-By: Shaan-Shoukath <shaanshoukath4522@gmail.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The page read as live operator instructions for `cutover_printing_kernel`, but 237e0f3
(B7c) deleted that command with the rest of the apps/printing runtime, and 911f458 (B7d)
deleted its read-only successor when it tombstoned the legacy tables. An operator
following it during a real incident would have hunted for a command that has not existed
since July. Reframed as a record: what the cutover did, what still exists
(PrintingCutoverRepair behind a read-only admin, the provenance migration), and the
forward-repair rollback boundary, which is worth keeping because the same rule governs
every append-only surface here.

Co-Authored-By: Shaan-Shoukath <shaanshoukath4522@gmail.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…make it opt-in

The operator-facing name was already "Member accounts"; only the key said `accounts`,
which read as ambiguous next to the separate `membership` module. The key is now
`member_accounts`, with a reversible data migration over Makerspace.enabled_modules -
mandatory rather than cosmetic, because _canonical_modules deliberately preserves unknown
keys, so an un-migrated row would have kept a dead `accounts` entry while the module
silently stopped working.

Both member_accounts and mobile lose default_enabled. Dropping it from member_accounts
alone would have been cosmetic: mobile declares requires_modules=("member_accounts",) and
install_module enables a key "and anything it requires", so a default install would have
pulled member accounts straight back in. A fresh makerspace now gets 8 modules - the six
core plus payments and updates - and staff sign in with email and password through
superadmin, Space Manager and custom roles, which never depended on this module. Existing
makerspaces are unaffected: migration 0057 already backfilled the key onto their rows.

Google and social auth are untouched - login methods live in PlatformLoginMethods behind
the /control/ switches, not behind a per-makerspace module.

Co-Authored-By: Shaan-Shoukath <shaanshoukath4522@gmail.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Codex GPT-5.6 <noreply@openai.com>
Written alongside the member_accounts rename rather than requested by it, so it lands
separately: a module with no group vanishes from the console it is administered from, and
an unknown group or a dependency on an unknown key should refuse at import.

Co-Authored-By: Shaan-Shoukath <shaanshoukath4522@gmail.com>
Co-Authored-By: Codex GPT-5.6 <noreply@openai.com>
Puts member accounts where operators look for it - under Membership in the console tick
list - without coupling the two. It stays an independent module and changes only its
group, because the alternative reading (making it a FeatureDefinition under membership)
carries a parent_module prerequisite, which would force a space running Keycloak or
walk-in records only to enable the member account ecosystem it deliberately does not
want. That independence is the point: the registry comment on `membership` already says
identity may instead come from external OIDC or a staff-created person record.

GROUP_ACCOUNTS held only this module, so an empty group heading would have been left
behind; the group is removed and the operator-facing half of its description - including
"staff always sign in with a password regardless" - folds into Membership's, where it is
now the reassurance that matters.

Co-Authored-By: Shaan-Shoukath <shaanshoukath4522@gmail.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Shaan-Shoukath <shaanshoukath4522@gmail.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`validate-compose-wrapper.py` used Path at module scope without importing it, so
it raised NameError before parsing an argument. spaceworks-compose.sh runs it
under `set -euo pipefail` before `exec docker compose`, which aborted every
compose command on every topology: setup, update, restore and backup import.
Not on origin/main, so nothing deployed was affected.

The guard executes every shipped Python script's module scope, which is the
failure class; `main()` stays behind its __main__ guard so scripts with
different CLI conventions are not held to argparse's exit codes.

Co-Authored-By: Shaan-Shoukath <shaanshoukath4522@gmail.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…pullable

Every published tag was unpullable: `docker pull` failed with "manifest unknown"
for latest, main, 0.5 and the pinned release, on both images. build-push-action
leaves provenance on, so each tag is an OCI index whose platform and attestation
manifests are separate UNTAGGED package versions; the tag-only keep-list deleted
exactly those and left the indexes dangling.

Retention moves to scripts/ghcr-retention.py, which decides by digest: a kept
root protects every child in its index, and a child-discovery failure vetoes
deletion for that whole package. Fail-closed is the invariant, not caution -
treating an inspection error as "no children" reproduces the outage. The step is
continue-on-error, so a silently-never-deleting implementation is invisible in
CI; the fixture tests cover both directions and verify-release-images.sh runs
after cleanup so a release that orphans its images fails at the release.

Co-Authored-By: Shaan-Shoukath <shaanshoukath4522@gmail.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Codex GPT-5.6 <noreply@openai.com>
@Shaan-Shoukath
Shaan-Shoukath merged commit 5bf555b into main Sep 1, 2026
1 check passed
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