diff --git a/WORK-RECORDS.md b/WORK-RECORDS.md index abdac06..c2ba4af 100644 --- a/WORK-RECORDS.md +++ b/WORK-RECORDS.md @@ -92,31 +92,35 @@ | task | CSOC-WP-0012-T04 | progress | — | workplans/CSOC-WP-0012-durable-recovery-service-readiness.md | | task | CSOC-WP-0012-T05 | done | — | workplans/CSOC-WP-0012-durable-recovery-service-readiness.md | | task | CSOC-WP-0012-T06 | done | — | workplans/CSOC-WP-0012-durable-recovery-service-readiness.md | -| task | CSOC-WP-0012-T07 | wait | — | workplans/CSOC-WP-0012-durable-recovery-service-readiness.md | -| task | CSOC-WP-0012-T08 | wait | — | workplans/CSOC-WP-0012-durable-recovery-service-readiness.md | +| task | CSOC-WP-0012-T07 | done | — | workplans/CSOC-WP-0012-durable-recovery-service-readiness.md | +| task | CSOC-WP-0012-T08 | done | — | workplans/CSOC-WP-0012-durable-recovery-service-readiness.md | | task | CSOC-WP-0012-T09 | todo | — | workplans/CSOC-WP-0012-durable-recovery-service-readiness.md | | task | CSOC-WP-0012-T10 | todo | — | workplans/CSOC-WP-0012-durable-recovery-service-readiness.md | -| task | CSOC-WP-0012-T11 | todo | — | workplans/CSOC-WP-0012-durable-recovery-service-readiness.md | -| task | CSOC-WP-0012-T12 | todo | — | workplans/CSOC-WP-0012-durable-recovery-service-readiness.md | +| task | CSOC-WP-0012-T11 | progress | — | workplans/CSOC-WP-0012-durable-recovery-service-readiness.md | +| task | CSOC-WP-0012-T12 | progress | — | workplans/CSOC-WP-0012-durable-recovery-service-readiness.md | | task | CSOC-WP-0012-T13 | done | — | workplans/CSOC-WP-0012-durable-recovery-service-readiness.md | +| task | CSOC-WP-0012-T14 | done | — | workplans/CSOC-WP-0012-durable-recovery-service-readiness.md | +| task | CSOC-WP-0012-T15 | done | — | workplans/CSOC-WP-0012-durable-recovery-service-readiness.md | | task | CSOC-WP-0013-T01 | done | — | workplans/CSOC-WP-0013-sharing-content-hardening.md | | task | CSOC-WP-0013-T02 | progress | — | workplans/CSOC-WP-0013-sharing-content-hardening.md | -| task | CSOC-WP-0013-T03 | todo | — | workplans/CSOC-WP-0013-sharing-content-hardening.md | +| task | CSOC-WP-0013-T03 | progress | — | workplans/CSOC-WP-0013-sharing-content-hardening.md | | task | CSOC-WP-0013-T04 | done | — | workplans/CSOC-WP-0013-sharing-content-hardening.md | -| task | CSOC-WP-0013-T05 | todo | — | workplans/CSOC-WP-0013-sharing-content-hardening.md | +| task | CSOC-WP-0013-T05 | progress | — | workplans/CSOC-WP-0013-sharing-content-hardening.md | +| task | CSOC-WP-0013-T06 | progress | — | workplans/CSOC-WP-0013-sharing-content-hardening.md | | task | CSOC-WP-0014-T01 | progress | — | workplans/CSOC-WP-0014-cohort-transfer-bubble-offramp.md | | task | CSOC-WP-0014-T02 | todo | — | workplans/CSOC-WP-0014-cohort-transfer-bubble-offramp.md | | task | CSOC-WP-0014-T03 | wait | — | workplans/CSOC-WP-0014-cohort-transfer-bubble-offramp.md | | task | CSOC-WP-0014-T04 | done | — | workplans/CSOC-WP-0014-cohort-transfer-bubble-offramp.md | | task | CSOC-WP-0014-T05 | done | — | workplans/CSOC-WP-0014-cohort-transfer-bubble-offramp.md | | task | CSOC-WP-0014-T06 | progress | — | workplans/CSOC-WP-0014-cohort-transfer-bubble-offramp.md | -| task | CSOC-WP-0014-T07 | todo | — | workplans/CSOC-WP-0014-cohort-transfer-bubble-offramp.md | +| task | CSOC-WP-0014-T07 | progress | — | workplans/CSOC-WP-0014-cohort-transfer-bubble-offramp.md | +| task | CSOC-WP-0014-T08 | done | — | workplans/CSOC-WP-0014-cohort-transfer-bubble-offramp.md | | task | CSOC-WP-0015-T01 | done | — | workplans/CSOC-WP-0015-communication-runtime-acceptance.md | | task | CSOC-WP-0015-T02 | progress | — | workplans/CSOC-WP-0015-communication-runtime-acceptance.md | | task | CSOC-WP-0015-T03 | done | — | workplans/CSOC-WP-0015-communication-runtime-acceptance.md | | task | CSOC-WP-0015-T04 | done | — | workplans/CSOC-WP-0015-communication-runtime-acceptance.md | | task | CSOC-WP-0015-T05 | todo | — | workplans/CSOC-WP-0015-communication-runtime-acceptance.md | -| task | CSOC-WP-0015-T06 | todo | — | workplans/CSOC-WP-0015-communication-runtime-acceptance.md | +| task | CSOC-WP-0015-T06 | progress | — | workplans/CSOC-WP-0015-communication-runtime-acceptance.md | | task | CSOC-WP-0016-T01 | done | — | workplans/CSOC-WP-0016-mfa-setup-link.md | | task | CSOC-WP-0017-T01 | done | — | workplans/CSOC-WP-0017-wave2-resource-right-sizing.md | | intake | CSOC-IN-0001 | closed | blue | docs/intakes/csoc-residuals.md | diff --git a/coulomb_social/apps/spaces/assets.py b/coulomb_social/apps/spaces/assets.py index c4d8572..61d77d4 100644 --- a/coulomb_social/apps/spaces/assets.py +++ b/coulomb_social/apps/spaces/assets.py @@ -83,6 +83,13 @@ def open_local_image(space_slug: str, path: str): def local_asset_url(space_slug: str, reference: str) -> str: """Map space-root or page-relative assets to the authenticated app route.""" + path = local_asset_path(reference) + if not path or PurePosixPath(path).suffix.lower() not in _TYPES: + return "" + return reverse("spaces:asset", kwargs={"slug": space_slug, "asset_path": path}) + + +def local_asset_path(reference: str) -> str: try: parsed = urlsplit(reference) except ValueError: @@ -98,11 +105,16 @@ def local_asset_url(space_slug: str, reference: str) -> str: return "" try: asset_parts(path) - if PurePosixPath(path).suffix.lower() not in _TYPES: - return "" except ValueError: return "" - return reverse("spaces:asset", kwargs={"slug": space_slug, "asset_path": path}) + return path + + +def local_download_url(space_slug: str, reference: str) -> str: + path = local_asset_path(reference) + if not path: + return "" + return reverse("spaces:download", kwargs={"slug": space_slug, "asset_path": path}) class _AssetLinks(HTMLParser): @@ -115,7 +127,9 @@ class _AssetLinks(HTMLParser): result = [] for name, value in attrs: if (tag, name) in {("img", "src"), ("a", "href")} and value: - value = local_asset_url(self.space_slug, value) or value + value = (local_asset_url(self.space_slug, value) + or (local_download_url(self.space_slug, value) if tag == "a" else "") + or value) result.append(name if value is None else f'{name}="{escape(value, quote=True)}"') self.output.append("<" + tag + (" " + " ".join(result) if result else "") + ">") diff --git a/coulomb_social/apps/spaces/attachments.py b/coulomb_social/apps/spaces/attachments.py new file mode 100644 index 0000000..8a907c0 --- /dev/null +++ b/coulomb_social/apps/spaces/attachments.py @@ -0,0 +1,196 @@ +"""Page attachments: immutable files, revisioned associations and bounded uploads.""" + +import hashlib +import json +import os +import re +import uuid +from html.parser import HTMLParser +from pathlib import Path + +from django.utils.text import slugify + +from . import pageops +from .assets import ( + asset_parts, + local_asset_path, + local_asset_url, + local_download_url, + open_local_asset, +) + +KEY = "coulomb_attachments" +MAX_UPLOAD_BYTES = 16 * 1024 * 1024 +MAX_ATTACHMENTS = 100 + + +def page_files(space, doc): + """List page associations plus existing body/Visual references, without I/O.""" + from .content import render_markdown + + files = {local_asset_path(e["path"]): {**e, "managed": True} for e in entries(doc.meta)} + + def add_reference(value): + path = local_asset_path(value) + if path and path not in files: + files[path] = {"path": "assets/" + path, "name": Path(path).name, "managed": False} + + class Links(HTMLParser): + def handle_starttag(self, tag, attrs): + for key, value in attrs: + if (tag, key) in {("a", "href"), ("img", "src")} and value: + add_reference(value) + + parser = Links() + parser.feed(render_markdown(doc.body)) + parser.close() + add_reference(doc.visual) + return [{**entry, "download_url": local_download_url(space, entry["path"]), + "open_url": local_asset_url(space, entry["path"])} for entry in files.values()] + + +def entries(meta): + raw = meta.get(KEY, "[]") + try: + result = json.loads(raw) + if not isinstance(result, list) or len(result) > MAX_ATTACHMENTS: + raise ValueError + paths = set() + for entry in result: + if (not isinstance(entry, dict) or set(entry) != {"name", "path"} + or not isinstance(entry["name"], str) or not entry["name"] + or len(entry["name"]) > 200 or not isinstance(entry["path"], str) + or not entry["path"].startswith("assets/")): + raise ValueError + asset_parts(entry["path"][7:]) + if entry["path"] in paths: + raise ValueError + paths.add(entry["path"]) + return result + except (ValueError, TypeError, KeyError) as exc: + raise pageops.PageOpsError("The attachment list needs operator review") from exc + + +def _metadata(space, doc, files): + meta = dict(title=doc.title, abstractor=doc.abstractor, visual=doc.visual, + space=space) + meta.update({k: v for k, v in doc.meta.items() if k not in meta}) + meta[KEY] = json.dumps(files, ensure_ascii=True) + return meta + + +def _current(space, page, revision): + doc = pageops.read_page(space, page) + if not doc.exists: + raise pageops.PageOpsError("Page not found") + if not revision or doc.revision != revision: + raise pageops.PageOpsError("This page changed. Reopen it before changing attachments") + return doc + + +def _save(space, doc, meta): + return pageops.write_page( + space, doc.slug, title=doc.title, abstractor=doc.abstractor, + visual=doc.visual, body=doc.body, extra_meta=meta, expected_revision=doc.revision, + ) + + +@pageops.content_writer +def add(space, page, upload, *, revision): + doc = _current(space, page, revision) + files = entries(doc.meta) + if len(files) >= MAX_ATTACHMENTS: + raise pageops.PageOpsError("This page already has 100 attachments") + if upload is None or upload.size > MAX_UPLOAD_BYTES: + raise pageops.PageOpsError("Choose a file no larger than 16 MiB") + raw = upload.read(MAX_UPLOAD_BYTES + 1) + if not raw or len(raw) > MAX_UPLOAD_BYTES: + raise pageops.PageOpsError("Choose a non-empty file no larger than 16 MiB") + label = Path(upload.name.replace("\\", "/")).name + label = "".join(c for c in label if c.isprintable())[:200] or "attachment" + suffix = Path(label).suffix.lower() + if not re.fullmatch(r"\.[a-z0-9]{1,12}", suffix): + suffix = "" + stem = slugify(Path(label).stem)[:80] or "attachment" + identity = uuid.uuid4().hex + name = f"upload-{identity}-{stem}{suffix}" + path = pageops.assets_dir(space) / name + # Exclusive creation and the shared writer lock preserve every older asset. + if path.exists() or path.is_symlink(): + raise pageops.PageOpsError("Attachment address already exists") + meta = _metadata(space, doc, [*files, {"name": label, "path": "assets/" + name}]) + record = { + "id": identity, "version": 1, "op": "attachment_upload", "phase": "started", + "source_space": space, "dest_space": space, + "source_page": doc.slug, "dest_page": doc.slug, + "source_revision": doc.revision, + "dest_revision": hashlib.sha256( + pageops.dump_frontmatter(meta, doc.body).encode()).hexdigest(), + "assets": [name], "asset_sha256": hashlib.sha256(raw).hexdigest(), + } + pageops._write_record(record) + try: + with open_local_asset(space, name, create=True) as stream: + stream.write(raw) + stream.flush() + os.fsync(stream.fileno()) + pageops._fsync_directory(path.parent) + result = _save(space, doc, meta) + if result.revision != record["dest_revision"]: + raise pageops.PageOpsError("Attachment publication needs operator review") + pageops._clear_record(record) + return result + except BaseException: + pageops._recover_record(record) + raise + + +@pageops.content_writer +def remove(space, page, reference, *, revision): + doc = _current(space, page, revision) + files = entries(doc.meta) + remaining = [entry for entry in files if entry["path"] != reference] + if len(remaining) == len(files): + raise pageops.PageOpsError("Attachment not found on this page") + # File bytes remain available to retained revisions and other page references. + return _save(space, doc, _metadata(space, doc, remaining)) + + +def recover(record): + """Keep revision-referenced complete bytes; discard only this failed upload.""" + try: + identity = record["id"] + if not re.fullmatch(r"[a-f0-9]{32}", identity): + return "held" + space, page = record["source_space"], record["source_page"] + if (record.get("version") != 1 or record.get("phase") != "started" + or record["dest_space"] != space or record["dest_page"] != page + or len(record["assets"]) != 1): + return "held" + name = record["assets"][0] + if not name.startswith(f"upload-{identity}-") or len(asset_parts(name)) != 1: + return "held" + for field in ("source_revision", "dest_revision", "asset_sha256"): + if not re.fullmatch(r"[a-f0-9]{64}", record[field]): + return "held" + current = pageops.page_file(space, page) + if pageops.assets_dir(space).is_symlink(): + return "held" + revision = pageops.content_root() / ".revisions" / space / page / ( + record["dest_revision"] + ".md") + if any(p.is_symlink() for p in (revision, *revision.parents)): + return "held" + if revision.exists(): + if pageops._file_revision(revision) != record["dest_revision"]: + return "held" + with open_local_asset(space, name) as stream: + if hashlib.file_digest(stream, "sha256").hexdigest() != record["asset_sha256"]: + return "held" + elif current.is_file() and pageops._file_revision(current) == record["source_revision"]: + pageops._discard_recorded_assets(space, [name]) + else: + return "held" + pageops._clear_record(record) + return "cleared" + except (KeyError, TypeError, ValueError, OSError, pageops.PageOpsError): + return "held" diff --git a/coulomb_social/apps/spaces/bubble_bundle.py b/coulomb_social/apps/spaces/bubble_bundle.py index 590cc00..66c1767 100644 --- a/coulomb_social/apps/spaces/bubble_bundle.py +++ b/coulomb_social/apps/spaces/bubble_bundle.py @@ -143,6 +143,18 @@ def prepare_bundle(spaces, articles, *, inventory_spaces=None): detail["relations"]["context_custom_article"] = normalize_ref(parent) if parent else None if article.get("hasattachments_boolean"): detail["review_flags"].append("attachment_records_require_capture") + if "_list_custom_scrap" in article: + # Bubble may return a deferred LOOKUP string instead of the actual + # list. Preserve it privately; it is not a captured scrap record or + # an article ID that may be traversed as a chunk. + raw_scraps = article["_list_custom_scrap"] + detail["scrap_relation"] = { + "raw": raw_scraps, + "state": "observed_empty_list" if raw_scraps == [] else "unresolved", + "records_captured": False, + } + if raw_scraps != []: + detail["review_flags"].append("scrap_relation_records_require_capture") article_details[identity] = detail for entry in plan["spaces"]: diff --git a/coulomb_social/apps/spaces/content.py b/coulomb_social/apps/spaces/content.py index dbebc5b..d7ff175 100644 --- a/coulomb_social/apps/spaces/content.py +++ b/coulomb_social/apps/spaces/content.py @@ -52,6 +52,7 @@ _ALLOWED_ATTRS = { "img": ["src", "alt", "title"], "a": ["href", "title", "rel"], "code": ["class"], + "ol": ["start"], "th": ["align"], "td": ["align"], } @@ -100,7 +101,8 @@ def render_markdown(text: str) -> str: """Render the supported Markdown subset. Extensions are fenced code, tables, newline-to-break and sane lists. - Raw HTML, script, event handlers and non-http(s)/mailto URLs are removed. + Ordered-list starting numbers are preserved. Unsupported HTML, scripts, + event handlers and non-http(s)/mailto URLs are removed. Relative asset URLs are kept so copied page images still resolve. This is not acceptance of a private corpus or of arbitrary Bubble markup. """ diff --git a/coulomb_social/apps/spaces/import_inspection.py b/coulomb_social/apps/spaces/import_inspection.py new file mode 100644 index 0000000..dc2e8ef --- /dev/null +++ b/coulomb_social/apps/spaces/import_inspection.py @@ -0,0 +1,181 @@ +"""Read-only classification of uncertain corpus imports; never repairs a store.""" + +from __future__ import annotations + +import fcntl +import hashlib +import os +import uuid +from contextlib import contextmanager + +from django.db import connection, transaction + +from . import pageops +from .import_receipts import digest, private_path, tree_digest +from .import_tree import _metadata, _source_dates +from .models import Space, SpaceMembership + + +@contextmanager +def existing_lock(path): + private_path(path) + # Inspection never creates a lock, journal, content root or receipt directory. + fd = os.open(path, os.O_RDONLY | os.O_NOFOLLOW) + try: + try: + fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB) + except BlockingIOError as exc: + raise ValueError("Import or content writer is active; inspect after it stops") from exc + yield + finally: + os.close(fd) + + +def expected_metadata(tree, tenant, slug, grants): + title, description, visual = _metadata(tree, slug) + values = { + "tenant_id": tenant, + "slug": slug, + "title": title, + "description": description, + "visual": visual, + "is_active": True, + "content_root": "pages", + "default_branch": "main", + "forgejo_owner": "", + "forgejo_repo": "", + "created_by_id": next(m.pk for m, role in grants if role == SpaceMembership.Role.OWNER), + } + index = tree / "pages/index.md" + if index.is_file(): + meta, _ = pageops.parse_frontmatter(index.read_text(encoding="utf-8")) + values.update(_source_dates(meta)) + return values + + +def observe_unit(receipts, index, unit): + tree, tenant, slug, grants = receipts.plan[index] + rows = list(Space.objects.filter(slug=slug).values()) + destination = pageops.space_dir(slug) + if destination.is_symlink() or any(p.is_symlink() for p in destination.parents): + raise ValueError("Destination must not contain symlinks") + files_hash = tree_digest(destination) if destination.is_dir() else None + grant_rows = sorted( + (str(member), role) + for member, role in SpaceMembership.objects.filter(space__slug=slug).values_list( + "member_id", "role" + ) + ) + result = { + "index": index, + "checkpoint_state": unit.get("state"), + "sql_rows": len(rows), + "published_tree_present": destination.exists(), + "files_sha256": files_hash, + "space_sha256": digest(rows), + "grants_sha256": digest(grant_rows), + } + state = unit.get("state") + if state == "completed": + result["classification"] = ( + "completed_identity_present" + if len(rows) == 1 + and str(rows[0]["id"]) == unit.get("space_id") + and rows[0]["tenant_id"] == tenant + else "completed_identity_changed" + ) + return result + if state not in {"pending", "started"}: + raise ValueError("Invalid checkpoint state") + if state == "pending": + result["classification"] = ( + "pending_destination_occupied" if rows or destination.exists() else "pending" + ) + return result + try: + reserved = uuid.UUID(unit["space_id"]) + except (KeyError, ValueError, TypeError, AttributeError): + result["classification"] = "started_identity_unbound" + return result + if not rows and not destination.exists(): + result["classification"] = "no_sql_or_published_tree" + return result + if not rows: + result["classification"] = "files_without_sql" + return result + if len(rows) != 1 or rows[0]["id"] != reserved or rows[0]["tenant_id"] != tenant: + result["classification"] = "foreign_destination_identity" + return result + if not destination.exists(): + result["classification"] = "sql_without_files" + return result + wanted = expected_metadata(tree, tenant, slug, grants) + metadata_match = all(rows[0][key] == value for key, value in wanted.items()) + grants_match = grant_rows == sorted((str(m.pk), role) for m, role in grants) + # The importer copies root Markdown pages and the assets subtree. Extra + # source files, or a synthesized index, cannot be called byte-exact here. + files_match = files_hash == receipts.source_hashes[index] + result.update( + metadata_matches=metadata_match, + grants_match=grants_match, + source_files_match=files_match, + original_space_date_checked="created_at" in wanted, + ) + result["classification"] = ( + "reserved_destination_matches_source" + if metadata_match and grants_match and files_match + else "reserved_destination_differs" + ) + return result + + +@contextmanager +def inspection_session(receipts): + if connection.in_atomic_block or not connection.get_autocommit(): + raise ValueError("Inspection requires an independent read transaction") + private_path(receipts.path) + if not receipts.path.is_file(): + raise ValueError("An existing checkpoint is required for inspection") + with ( + existing_lock(receipts.directory / ".lock"), + existing_lock(pageops.content_root() / ".writer.lock"), + ): + checkpoint = receipts.path.read_bytes() + data = receipts.read() + if any(not isinstance(unit, dict) for unit in data["spaces"]): + raise ValueError("Invalid space checkpoint") + with transaction.atomic(): + if connection.vendor == "postgresql": + with connection.cursor() as cursor: + cursor.execute("SET TRANSACTION ISOLATION LEVEL REPEATABLE READ, READ ONLY") + first = [observe_unit(receipts, i, unit) for i, unit in enumerate(data["spaces"])] + second = [observe_unit(receipts, i, unit) for i, unit in enumerate(data["spaces"])] + if first != second or checkpoint != receipts.path.read_bytes(): + raise ValueError("Destination or checkpoint changed during inspection") + if any( + tree_digest(tree) != checksum + for (tree, _, _, _), checksum in zip( + receipts.plan, receipts.source_hashes, strict=True + ) + ): + raise ValueError("Source changed during inspection") + staging = list((pageops.content_root() / "spaces").glob(".import-*")) + result = { + "schema": "coulomb.corpus-import-inspection.v1", + "binding": receipts.binding, + "checkpoint_sha256": hashlib.sha256(checkpoint).hexdigest(), + "spaces": first, + "unattributed_staging_entries": len(staging), + "read_only": True, + "external_writer_fence_verified": False, + "checkpoint_completed": False, + "retry_authorized": False, + "data_restored": False, + } + result["inspection_sha256"] = digest(result) + yield result, data + + +def inspect(receipts): + with inspection_session(receipts) as (result, _): + return result diff --git a/coulomb_social/apps/spaces/import_receipts.py b/coulomb_social/apps/spaces/import_receipts.py new file mode 100644 index 0000000..707623c --- /dev/null +++ b/coulomb_social/apps/spaces/import_receipts.py @@ -0,0 +1,235 @@ +"""Opt-in corpus checkpoints; no automatic repair of uncertain DB/file commits. + +The caller still owns source freezing, external writer fencing, paired backups +and admission. A started receipt survives a crash and stops automatic retries. +Completed receipts skip their space, preserving any subsequent local edits. +""" + +from __future__ import annotations + +import fcntl +import hashlib +import json +import os +import re +import stat +import tempfile +import uuid +from contextlib import contextmanager +from pathlib import Path + +from django.db import connection + +from . import pageops +from .import_tree import import_space_tree +from .models import Space, SpaceMembership + +SCHEMA = "coulomb.corpus-import-checkpoint.v1" + + +def digest(value): + return hashlib.sha256(json.dumps(value, sort_keys=True, default=str).encode()).hexdigest() + + +def tree_digest(tree): + files = [] + for path in sorted(tree.rglob("*")): + mode = path.lstat().st_mode + if stat.S_ISDIR(mode): + continue + if not stat.S_ISREG(mode): + raise ValueError("Import tree must contain only real files and directories") + with path.open("rb") as stream: + checksum = hashlib.file_digest(stream, "sha256").hexdigest() + files.append([path.relative_to(tree).as_posix(), checksum]) + return digest(files) + + +def private_path(path, *, directory=False): + if any(p.is_symlink() for p in (path, *path.parents)): + raise ValueError("Receipt path must not contain symlinks") + if path.exists(): + info = path.stat() + expected = stat.S_ISDIR if directory else stat.S_ISREG + if (not expected(info.st_mode) or info.st_uid != os.getuid() + or info.st_mode & 0o077 or (not directory and info.st_nlink != 1)): + raise ValueError("Receipt path must be private and owned by this operator") + + +def sync_directory(path): + fd = os.open(path, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW) + try: + os.fsync(fd) + finally: + os.close(fd) + + +def sync_tree(tree): + # Content must be durable before its completed receipt. The underlying + # importer commits SQL first; a crash before this finishes remains started. + directories = [tree] + for path in tree.rglob("*"): + mode = path.lstat().st_mode + if stat.S_ISDIR(mode): + directories.append(path) + elif stat.S_ISREG(mode): + fd = os.open(path, os.O_RDONLY | os.O_NOFOLLOW) + try: + os.fsync(fd) + finally: + os.close(fd) + else: + raise ValueError("Imported tree contains an unsupported file") + for directory in sorted(directories, key=lambda p: len(p.parts), reverse=True): + sync_directory(directory) + sync_directory(tree.parent) + sync_directory(tree.parent.parent) + + +class CorpusReceipts: + def __init__(self, directory, manifest, plan): + self.directory = Path(directory).expanduser().absolute() + private_path(self.directory, directory=True) + self.directory = self.directory.resolve() + root = pageops.content_root().resolve() + for protected in (root, *(tree for tree, _, _, _ in plan)): + if (self.directory.is_relative_to(protected) + or protected.is_relative_to(self.directory)): + raise ValueError("Receipt directory must be separate from source and content") + self.path = self.directory / "checkpoint.json" + self.plan = plan + self.source_hashes = [tree_digest(tree) for tree, _, _, _ in plan] + self.binding = digest({ + "manifest": hashlib.sha256(Path(manifest).read_bytes()).hexdigest(), + "content_root": str(root), + "database": {key: connection.settings_dict.get(key) + for key in ("ENGINE", "HOST", "PORT", "NAME", "USER")}, + "units": [ + [str(tree), tenant, slug, source_hash, + [[str(member.pk), role] for member, role in grants]] + for (tree, tenant, slug, grants), source_hash + in zip(plan, self.source_hashes, strict=True) + ], + }) + + def read(self): + private_path(self.directory, directory=True) + private_path(self.path) + if not self.path.exists(): + return {"schema": SCHEMA, "binding": self.binding, + "spaces": [{"state": "pending"} for _ in self.plan]} + data = json.loads(self.path.read_bytes()) + if (not isinstance(data, dict) or data.get("schema") != SCHEMA + or data.get("binding") != self.binding + or not isinstance(data.get("spaces"), list) + or len(data["spaces"]) != len(self.plan)): + raise ValueError("Receipt does not match this corpus, mapping or destination") + return data + + def validate(self, data): + for unit, (_, tenant, slug, _) in zip(data["spaces"], self.plan, strict=True): + if not isinstance(unit, dict): + raise ValueError("Invalid space checkpoint") + state = unit.get("state") + if state == "completed": + try: + uuid.UUID(unit["space_id"]) + if any(not re.fullmatch(r"[a-f0-9]{64}", unit[key]) + for key in ("files_sha256", "space_sha256", "grants_sha256")): + raise ValueError + if any(type(unit[key]) is not int or unit[key] < 0 + for key in ("pages", "assets")): + raise ValueError + except (KeyError, TypeError, ValueError, AttributeError) as exc: + raise ValueError("Invalid completed space checkpoint") from exc + # Never recreate an independently deleted/replaced space or infer + # that a later edit or grant change should be reversed. + if not Space.objects.filter(pk=unit.get("space_id"), tenant_id=tenant, + slug=slug).exists(): + raise ValueError("Completed space identity changed; reconciliation required") + elif state == "pending": + if (Space.objects.filter(slug=slug).exists() + or pageops.space_dir(slug).exists()): + raise ValueError("Unreceipted destination exists; reconciliation required") + else: + raise ValueError( + "Uncertain space import; reconcile the started receipt before retry") + + def save(self, data): + private_path(self.path) + fd, temporary = tempfile.mkstemp(prefix=".checkpoint-", dir=self.directory) + try: + with os.fdopen(fd, "w") as stream: + json.dump(data, stream, sort_keys=True) + stream.flush() + os.fsync(stream.fileno()) + os.replace(temporary, self.path) + sync_directory(self.directory) + finally: + if os.path.exists(temporary): + os.unlink(temporary) + + @contextmanager + def lock(self): + # Parent must already exist; do not chmod an operator's existing path. + self.directory.mkdir(mode=0o700, exist_ok=True) + private_path(self.directory, directory=True) + sync_directory(self.directory.parent) + lock = self.directory / ".lock" + private_path(lock) + fd = os.open(lock, os.O_CREAT | os.O_RDWR | os.O_NOFOLLOW, 0o600) + try: + try: + fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB) + except BlockingIOError as exc: + raise ValueError("Another importer owns this receipt directory") from exc + yield + finally: + os.close(fd) + + def run(self): + if connection.in_atomic_block or not connection.get_autocommit(): + raise ValueError("Receipted import requires independent database commits") + self.validate(self.read()) # Invalid inputs remain write-free. + with self.lock(), pageops.content_write_lock(): + data = self.read() + self.validate(data) + pageops.recover_interrupted_content() + self.save(data) + imported = skipped = 0 + for index, (tree, tenant, slug, grants) in enumerate(self.plan): + if data["spaces"][index]["state"] == "completed": + skipped += 1 + continue + if tree_digest(tree) != self.source_hashes[index]: + raise ValueError("Source changed after preflight") + # Persist uncertainty BEFORE the first import write. Any failure, + # including a lost commit reply or receipt fsync, stays uncertain. + space_id = uuid.uuid4() + data["spaces"][index] = {"state": "started", "space_id": str(space_id)} + self.save(data) + report = import_space_tree(tree, tenant_id=tenant, slug=slug, + memberships=grants, replace=False, + new_space_id=space_id) + if tree_digest(tree) != self.source_hashes[index]: + raise ValueError("Source changed during import; reconciliation required") + space = Space.objects.get(tenant_id=tenant, slug=slug) + if space.pk != space_id: + raise ValueError("Imported space identity changed; reconciliation required") + observed_grants = sorted((str(m), role) for m, role in + SpaceMembership.objects.filter(space=space).values_list("member_id", "role")) + if observed_grants != sorted((str(m.pk), role) for m, role in grants): + raise ValueError("Imported grants differ from the reviewed mapping") + sync_tree(pageops.space_dir(slug)) + # A fingerprint records the committed return, not acceptance of + # source fidelity or authorization to import more cohorts. + data["spaces"][index] = { + "state": "completed", "space_id": str(space.pk), + "files_sha256": tree_digest(pageops.space_dir(slug)), + "space_sha256": digest(Space.objects.filter(pk=space.pk).values().get()), + "grants_sha256": digest(observed_grants), + "pages": report.pages_copied, "assets": report.assets_copied, + } + self.save(data) + imported += 1 + return imported, skipped diff --git a/coulomb_social/apps/spaces/import_reconciliation.py b/coulomb_social/apps/spaces/import_reconciliation.py new file mode 100644 index 0000000..f8546df --- /dev/null +++ b/coulomb_social/apps/spaces/import_reconciliation.py @@ -0,0 +1,72 @@ +"""Explicit checkpoint completion for an exactly observed, already committed import. + +No SQL/content repair or automatic retry. The deployment owner still supplies +admission, a paired backup and an external writer fence before invoking this. +""" + +import hashlib +import re + +from . import pageops +from .import_inspection import inspection_session, observe_unit +from .import_receipts import digest, sync_tree, tree_digest +from .models import Space + + +def complete_matching_commit(receipts, *, index, expected_inspection): + if (type(index) is not int or not 0 <= index < len(receipts.plan) + or not isinstance(expected_inspection, str) + or not re.fullmatch(r"[a-f0-9]{64}", expected_inspection)): + raise ValueError("An exact inspection digest and valid unit index are required") + with inspection_session(receipts) as (inspection, data): + if inspection["inspection_sha256"] != expected_inspection: + raise ValueError("Inspection changed; retain evidence and review again") + observed = inspection["spaces"][index] + if observed["classification"] != "reserved_destination_matches_source": + raise ValueError("Only an exact reserved committed import can be completed") + if inspection["unattributed_staging_entries"]: + raise ValueError("Unattributed staging requires owner reconciliation first") + tree, _, slug, _ = receipts.plan[index] + prior_unit = data["spaces"][index] + row = Space.objects.filter(pk=prior_unit["space_id"]).values().get() + if digest([row]) != observed["space_sha256"]: + raise ValueError("Observed SQL identity changed") + # The original importer can die before fsync or its completed checkpoint. + # Make the matching published tree durable before acknowledging it. + sync_tree(pageops.space_dir(slug)) + if (observe_unit(receipts, index, prior_unit) != observed + or tree_digest(tree) != receipts.source_hashes[index] + or hashlib.sha256(receipts.path.read_bytes()).hexdigest() + != inspection["checkpoint_sha256"]): + raise ValueError("State changed before checkpoint completion") + data["spaces"][index] = { + "state": "completed", + "space_id": prior_unit["space_id"], + "files_sha256": observed["files_sha256"], + "space_sha256": digest(row), + "grants_sha256": observed["grants_sha256"], + "pages": len(list((tree / "pages").glob("*.md"))), + "assets": sum(p.is_file() for p in (tree / "assets").rglob("*")), + "reconciliation": { + "schema": "coulomb.corpus-matching-commit-reconciliation.v1", + "prior_unit": prior_unit, + "inspection": inspection, + }, + } + # Existing atomic replacement + fsync keeps the prior started checkpoint + # or the completed one with its evidence. Lost output requires readback; + # replay of the old inspection cannot acknowledge a different state. + receipts.save(data) + return { + "schema": "coulomb.corpus-matching-commit-return.v1", + "status": "matching_commit_checkpoint_completed", + "index": index, + "inspection_sha256": expected_inspection, + "checkpoint_sha256": hashlib.sha256(receipts.path.read_bytes()).hexdigest(), + "checkpoint_completed": True, + "sql_modified": False, + "content_modified": False, + "data_restored": False, + "external_writer_fence_verified": False, + "remaining_import_authorized": False, + } diff --git a/coulomb_social/apps/spaces/import_tree.py b/coulomb_social/apps/spaces/import_tree.py index 1b64011..767427f 100644 --- a/coulomb_social/apps/spaces/import_tree.py +++ b/coulomb_social/apps/spaces/import_tree.py @@ -4,6 +4,7 @@ from __future__ import annotations import shutil import tempfile +import uuid from dataclasses import dataclass, field from pathlib import Path @@ -63,11 +64,15 @@ def _metadata(tree: Path, slug: str, title: str | None = None) -> tuple[str, str def validate_space_tree( tree: Path, *, tenant_id: str, slug: str | None = None, member: Member | None = None, replace: bool = False, title: str | None = None, + check_existing: bool = True, ) -> tuple[Path, str]: """Preflight used by dry-run and execution, before any content writes. CONTENT_ROOT is instance-local and keyed by slug, so importing a slug already owned by another tenant is unsupported and must fail closed. + Receipt-aware preflight may set check_existing=False to inspect sources for + completed units; it must validate destination identities separately. The + actual importer always performs the default destination checks again. """ tree = tree.expanduser() if tree.is_symlink(): @@ -115,9 +120,9 @@ def validate_space_tree( for path in (root, root / "spaces", dest): if path.is_symlink(): raise pageops.PageOpsError("Destination must not be a symlink") - if dest.exists() and not replace: + if check_existing and dest.exists() and not replace: raise pageops.PageOpsError("Destination already exists; pass replace=True to overwrite") - if dest.exists() and pageops.page_aliases(space_slug): + if check_existing and dest.exists() and pageops.page_aliases(space_slug): raise pageops.PageOpsError( "Retained page addresses need reconciliation before replacing content" ) @@ -128,7 +133,13 @@ def import_space_tree( tree: Path, *, tenant_id: str, slug: str | None = None, member: Member | None = None, title: str | None = None, replace: bool = False, memberships: list[tuple[Member, str]] | None = None, + new_space_id: uuid.UUID | None = None, ) -> ImportReport: + if new_space_id is not None: + if replace or not isinstance(new_space_id, uuid.UUID): + raise pageops.PageOpsError("Reserved import identity requires a new UUID space") + if Space.objects.filter(pk=new_space_id).exists(): + raise pageops.PageOpsError("Reserved import identity already exists") # Invalid proposals remain write-free, including lock-file creation. validate_space_tree(tree, tenant_id=tenant_id, slug=slug, member=member, replace=replace, title=title) @@ -136,7 +147,8 @@ def import_space_tree( if mapped.tenant_id != tenant_id or role not in SpaceMembership.Role.values: raise pageops.PageOpsError("Invalid membership tenant or role") return _import_space_tree(tree, tenant_id=tenant_id, slug=slug, member=member, - title=title, replace=replace, memberships=memberships) + title=title, replace=replace, memberships=memberships, + new_space_id=new_space_id) @pageops.content_writer @@ -144,6 +156,7 @@ def _import_space_tree( tree: Path, *, tenant_id: str, slug: str | None = None, member: Member | None = None, title: str | None = None, replace: bool = False, memberships: list[tuple[Member, str]] | None = None, + new_space_id: uuid.UUID | None = None, ) -> ImportReport: """Stage content, then publish with DB writes; restore on ordinary errors. @@ -190,15 +203,22 @@ def _import_space_tree( try: with transaction.atomic(): try: - space, created = Space.objects.update_or_create( - tenant_id=tenant_id, slug=space_slug, - defaults={"title": display_title, "description": abstractor, - "visual": visual, "is_active": True, - "content_root": "pages", "default_branch": "main"}, - ) + defaults = {"title": display_title, "description": abstractor, + "visual": visual, "is_active": True, + "content_root": "pages", "default_branch": "main"} + if new_space_id is not None: + # A receipted attempt owns only its pre-recorded UUID. + # Never update a row created concurrently at this slug. + space = Space.objects.create(id=new_space_id, tenant_id=tenant_id, + slug=space_slug, **defaults) + created = True + else: + space, created = Space.objects.update_or_create( + tenant_id=tenant_id, slug=space_slug, defaults=defaults) except IntegrityError as exc: raise pageops.PageOpsError( - "Content slug belongs to another tenant" + "Reserved import identity or destination already exists" + if new_space_id is not None else "Content slug belongs to another tenant" ) from exc report.space_created = created report.space_updated = not created diff --git a/coulomb_social/apps/spaces/management/commands/import_content_corpus.py b/coulomb_social/apps/spaces/management/commands/import_content_corpus.py index 2299dc0..bf7b5cc 100644 --- a/coulomb_social/apps/spaces/management/commands/import_content_corpus.py +++ b/coulomb_social/apps/spaces/management/commands/import_content_corpus.py @@ -9,6 +9,7 @@ from django.core.management.base import BaseCommand, CommandError from coulomb_social.apps.members.models import Member from coulomb_social.apps.spaces import pageops +from coulomb_social.apps.spaces.import_receipts import CorpusReceipts from coulomb_social.apps.spaces.import_tree import import_space_tree, validate_space_tree from coulomb_social.apps.spaces.models import SpaceMembership @@ -20,10 +21,18 @@ class Command(BaseCommand): parser.add_argument("manifest") parser.add_argument("--dry-run", action="store_true") parser.add_argument("--replace", action="store_true") + parser.add_argument("--receipt-dir", help="Private durable checkpoints for new spaces only") def handle(self, *args, **options): + if options["receipt_dir"] and options["replace"]: + raise CommandError("Receipted imports cannot replace existing spaces") try: - plan = self.preflight(Path(options["manifest"]), replace=options["replace"]) + plan = self.preflight(Path(options["manifest"]), replace=options["replace"], + check_existing=not options["receipt_dir"]) + receipts = None + if options["receipt_dir"]: + receipts = CorpusReceipts(options["receipt_dir"], options["manifest"], plan) + receipts.validate(receipts.read()) except (ValueError, KeyError, TypeError, OSError, pageops.PageOpsError) as exc: raise CommandError(f"Manifest preflight failed: {exc}") from exc pages = sum(len(list((tree / "pages").glob("*.md"))) for tree, _, _, _ in plan) @@ -31,6 +40,17 @@ class Command(BaseCommand): if options["dry_run"]: self.stdout.write("Dry run — no writes") return + if receipts is not None: + try: + imported, skipped = receipts.run() + except Exception as exc: + raise CommandError( + "Receipted import stopped; inspect the private checkpoint before retrying. " + "A started space has an uncertain outcome and must be reconciled." + ) from exc + self.stdout.write(self.style.SUCCESS( + f"Imported {imported} space(s); skipped {skipped} completed space(s)")) + return completed = 0 for tree, tenant, slug, grants in plan: try: @@ -52,7 +72,7 @@ class Command(BaseCommand): ) self.stdout.write(self.style.SUCCESS(f"Imported {completed} space(s)")) - def preflight(self, manifest: Path, *, replace: bool): + def preflight(self, manifest: Path, *, replace: bool, check_existing: bool = True): manifest = manifest.expanduser().resolve() data = json.loads(manifest.read_text(encoding="utf-8")) if (not isinstance(data, dict) or data.get("version") != 1 @@ -71,6 +91,7 @@ class Command(BaseCommand): tree = manifest.parent / tree tree, slug = validate_space_tree( tree, tenant_id=tenant, slug=entry.get("slug"), replace=replace, + check_existing=check_existing, ) if slug in slugs: raise ValueError("Duplicate destination content slug") diff --git a/coulomb_social/apps/spaces/management/commands/inspect_content_corpus.py b/coulomb_social/apps/spaces/management/commands/inspect_content_corpus.py new file mode 100644 index 0000000..8059119 --- /dev/null +++ b/coulomb_social/apps/spaces/management/commands/inspect_content_corpus.py @@ -0,0 +1,34 @@ +"""Inspect uncertain receipt-backed imports without changing content or checkpoints.""" + +import json +from pathlib import Path + +from django.core.management.base import BaseCommand, CommandError + +from coulomb_social.apps.spaces import pageops +from coulomb_social.apps.spaces.import_inspection import inspect +from coulomb_social.apps.spaces.import_receipts import CorpusReceipts +from coulomb_social.apps.spaces.management.commands.import_content_corpus import ( + Command as ImportCommand, +) + + +class Command(BaseCommand): + help = "Classify an existing corpus checkpoint; never repair or authorize a retry" + + def add_arguments(self, parser): + parser.add_argument("manifest") + parser.add_argument("--receipt-dir", required=True) + + def handle(self, *args, **options): + try: + manifest = Path(options["manifest"]) + plan = ImportCommand().preflight(manifest, replace=False, check_existing=False) + receipts = CorpusReceipts(options["receipt_dir"], manifest, plan) + result = inspect(receipts) + except (ValueError, KeyError, TypeError, OSError, pageops.PageOpsError) as exc: + # Do not disclose source paths, identities or row contents in diagnostics. + raise CommandError( + "Corpus inspection refused; retain the checkpoint and inspect privately" + ) from exc + self.stdout.write(json.dumps(result, sort_keys=True)) diff --git a/coulomb_social/apps/spaces/management/commands/reconcile_content_import.py b/coulomb_social/apps/spaces/management/commands/reconcile_content_import.py new file mode 100644 index 0000000..c17efcc --- /dev/null +++ b/coulomb_social/apps/spaces/management/commands/reconcile_content_import.py @@ -0,0 +1,42 @@ +"""Record one matching committed import; never restore or delete application data.""" + +import json +from pathlib import Path + +from django.core.management.base import BaseCommand, CommandError + +from coulomb_social.apps.spaces.import_receipts import CorpusReceipts +from coulomb_social.apps.spaces.import_reconciliation import complete_matching_commit +from coulomb_social.apps.spaces.management.commands.import_content_corpus import ( + Command as ImportCommand, +) + + +class Command(BaseCommand): + help = "Complete one exact matching import checkpoint after owner reconciliation review" + + def add_arguments(self, parser): + parser.add_argument("manifest") + parser.add_argument("--receipt-dir", required=True) + parser.add_argument("--unit-index", type=int, required=True) + parser.add_argument("--expect-inspection", required=True) + parser.add_argument("--complete-matching-commit", action="store_true", required=True) + + def handle(self, *args, **options): + if not options["complete_matching_commit"]: + raise CommandError("Explicit matching-commit completion is required") + try: + manifest = Path(options["manifest"]) + plan = ImportCommand().preflight(manifest, replace=False, check_existing=False) + receipts = CorpusReceipts(options["receipt_dir"], manifest, plan) + result = complete_matching_commit( + receipts, index=options["unit_index"], + expected_inspection=options["expect_inspection"], + ) + except Exception as exc: + # Failure can follow durable checkpoint replacement. Never advise replay + # or disclose source paths, identities or SQL values in the error. + raise CommandError( + "Reconciliation stopped; inspect the retained checkpoint before further action" + ) from exc + self.stdout.write(json.dumps(result, sort_keys=True)) diff --git a/coulomb_social/apps/spaces/pageops.py b/coulomb_social/apps/spaces/pageops.py index 0ad7b16..cc48cce 100644 --- a/coulomb_social/apps/spaces/pageops.py +++ b/coulomb_social/apps/spaces/pageops.py @@ -448,6 +448,9 @@ def _finish_transfer_record(record, source_path, dest_confirmed): def _recover_record(record): + if isinstance(record, dict) and record.get("op") == "attachment_upload": + from .attachments import recover + return recover(record) if record == "held" or not _record_is_actionable(record): logger.error("content recovery left a journal for operator review") return "held" @@ -661,7 +664,8 @@ _BODY_ASSET = re.compile( ) -def _copy_assets(source_space, dest_space, dest_page, visual, body, before_create=None): +def _copy_assets(source_space, dest_space, dest_page, visual, body, before_create=None, + attached=()): """Copy local Visual and Markdown link/image destinations as one staged set.""" from .assets import asset_parts, open_local_asset @@ -725,7 +729,8 @@ def _copy_assets(source_space, dest_space, dest_page, visual, body, before_creat body = _BODY_ASSET.sub( lambda match: match["prefix"] + copy_reference(match["url"]), body, ) - return visual, body, created + attached = [{**entry, "path": copy_reference(entry["path"])} for entry in attached] + return visual, body, created, attached except BaseException: for path in created: path.unlink(missing_ok=True) @@ -764,8 +769,11 @@ def copy_page( on_asset(name) try: - visual, body, created = _copy_assets( + from .attachments import KEY, entries + + visual, body, created, attached = _copy_assets( source_space, dest_space, dest_page, src.visual, src.body, note_asset, + attached=entries(src.meta), ) new_title = title if title is not None else (src.title or dest_page) if title is None and new_title == src.title and source_space == dest_space: @@ -774,7 +782,8 @@ def copy_page( dest_space, dest_page, title=new_title, abstractor=src.abstractor, visual=visual, body=body, expected_revision="", extra_meta={k: v for k, v in src.meta.items() - if k not in {"title", "abstractor", "visual", "space"}}, + if k not in {"title", "abstractor", "visual", "space", KEY}} + | ({KEY: json.dumps(attached)} if KEY in src.meta else {}), ) if record is not None: record["dest_revision"] = doc.revision diff --git a/coulomb_social/apps/spaces/rendering_audit.py b/coulomb_social/apps/spaces/rendering_audit.py index 9886745..e664e92 100644 --- a/coulomb_social/apps/spaces/rendering_audit.py +++ b/coulomb_social/apps/spaces/rendering_audit.py @@ -34,6 +34,11 @@ def audit_body(body: str): rendered = render_markdown(body) before, after = _Structure(raw), _Structure(rendered) removed_attrs = before.attributes - after.attributes + reference_attributes = {"href", "src", "poster", "data", "action"} + before_refs = Counter({key: count for key, count in before.attributes.items() + if key[1] in reference_attributes}) + after_refs = Counter({key: count for key, count in after.attributes.items() + if key[1] in reference_attributes}) text_changed = " ".join("".join(before.text).split()) != ( " ".join("".join(after.text).split())) markup_changed = before.tags != after.tags or before.attributes != after.attributes @@ -43,6 +48,11 @@ def audit_body(body: str): "sanitization_changed_html": raw != rendered, "markup_changed_by_sanitization": markup_changed, "serialization_only_change": raw != rendered and not markup_changed and not text_changed, + "reference_occurrences_before": sum(before_refs.values()), + "reference_occurrences_after": sum(after_refs.values()), + "reference_attributes_preserved": before_refs == after_refs, + "removed_reference_occurrences": sum((before_refs - after_refs).values()), + "added_reference_occurrences": sum((after_refs - before_refs).values()), "removed_tag_counts": dict(sorted((before.tags - after.tags).items())), "added_tag_counts": dict(sorted((after.tags - before.tags).items())), "removed_attributes": [ diff --git a/coulomb_social/apps/spaces/urls.py b/coulomb_social/apps/spaces/urls.py index 41991f2..17b995f 100644 --- a/coulomb_social/apps/spaces/urls.py +++ b/coulomb_social/apps/spaces/urls.py @@ -12,6 +12,9 @@ urlpatterns = [ ), path("new/", views.space_create, name="create"), path("/assets/", views.space_asset, name="asset"), + path("/downloads/", views.space_download, name="download"), + path("/pages//attachments/", + views.page_attachments, name="page_attachments"), path("/edit/", views.space_edit, name="edit"), path("/sharing/", views.space_sharing, name="sharing"), path("/archive/", views.space_archive, name="archive"), diff --git a/coulomb_social/apps/spaces/views.py b/coulomb_social/apps/spaces/views.py index 2f2056a..6e52366 100644 --- a/coulomb_social/apps/spaces/views.py +++ b/coulomb_social/apps/spaces/views.py @@ -2,6 +2,7 @@ import hashlib import hmac import json import logging +from pathlib import PurePosixPath from django.conf import settings from django.contrib import messages @@ -21,8 +22,8 @@ from django.views.decorators.http import require_http_methods, require_POST, req from coulomb_social.apps.core.principal import build_principal -from . import pageops -from .assets import local_asset_url, open_local_image +from . import attachments, pageops +from .assets import local_asset_url, open_local_asset, open_local_image from .content import ( clear_content_cache_for_repo, clear_content_cache_for_space, @@ -79,6 +80,81 @@ def space_asset(request: HttpRequest, slug: str, asset_path: str) -> HttpRespons return response +@login_required +@require_safe +def space_download(request, slug, asset_path): + principal, forbid = _principal_or_forbid(request, "space_asset") + if forbid: + return forbid + space = get_space_for_member(principal.get("member"), slug) + if space is None or Space.objects.filter(slug=slug).exclude(pk=space.pk).exists(): + return HttpResponseNotFound("Attachment not found.") + try: + file = open_local_asset(slug, asset_path) + except (OSError, ValueError): + return HttpResponseNotFound("Attachment not found.") + response = FileResponse(file, as_attachment=True, filename=PurePosixPath(asset_path).name, + content_type="application/octet-stream") + response["Cache-Control"] = "private, no-store" + response["X-Content-Type-Options"] = "nosniff" + response["Content-Security-Policy"] = "default-src 'none'; sandbox" + return response + + +@login_required +@require_http_methods(["GET", "POST"]) +def page_attachments(request, slug, page_slug): + principal, forbid = _principal_or_forbid( + request, "page_edit" if request.method == "POST" else "space_detail") + if forbid: + return forbid + member = principal.get("member") + space = get_space_for_member(member, slug) + if space is None: + return HttpResponseNotFound("Page not found.") + can_write = can_write_space(member, space) + if request.method == "POST" and not can_write: + return HttpResponseForbidden("You have read-only access to this space.") + try: + doc = pageops.read_page(slug, page_slug) + if not doc.exists: + return HttpResponseNotFound("Page not found.") + if request.method == "POST": + try: + length = int(request.META.get("CONTENT_LENGTH") or 0) + except (ValueError, TypeError): + return HttpResponse("Invalid upload length.", status=400) + if length < 0: + return HttpResponse("Invalid upload length.", status=400) + if length > attachments.MAX_UPLOAD_BYTES + 65536: + return HttpResponse("Choose a file no larger than 16 MiB.", status=413) + action = request.POST.get("action") + if action == "add": + attachments.add(slug, page_slug, request.FILES.get("file"), + revision=request.POST.get("revision")) + messages.success(request, "Attachment added.") + elif action == "remove": + attachments.remove(slug, page_slug, request.POST.get("reference"), + revision=request.POST.get("revision")) + messages.success( + request, "Attachment removed from this page. Earlier revisions retain it.") + else: + raise pageops.PageOpsError("Choose an attachment action") + clear_content_cache_for_space(space) + return redirect("spaces:page_attachments", slug=slug, page_slug=page_slug) + files = attachments.page_files(slug, doc) + except (pageops.PageOpsError, OSError) as exc: + messages.error(request, _mutation_error(exc)) + if request.method == "POST": + return redirect("spaces:page_attachments", slug=slug, page_slug=page_slug) + return HttpResponse("Attachments unavailable.", status=409) + response = render(request, "spaces/attachments.html", { + "space": space, "doc": doc, "files": files, "can_write": can_write, + }) + response["Cache-Control"] = "private, no-store" + return response + + def _principal_or_forbid(request: HttpRequest, resource: str): principal = build_principal(request, authz_resource_id=resource) if not principal["authz_allow"]: @@ -301,6 +377,14 @@ def page_edit(request: HttpRequest, slug: str, page_slug: str = "index") -> Http return redirect("spaces:detail", slug=slug) doc = pageops.read_page(space.slug, page_slug) + # Show files from the saved page even when a rejected edit redisplays the + # submitted fields. Attachment mutations keep their own revision checks. + try: + saved_files = attachments.page_files(space.slug, doc) if doc.exists else [] + attachment_error = "" + except (pageops.PageOpsError, OSError) as exc: + saved_files = [] + attachment_error = _mutation_error(exc) if request.method == "POST": title = (request.POST.get("title") or "").strip() abstractor = (request.POST.get("abstractor") or "").strip() @@ -345,6 +429,8 @@ def page_edit(request: HttpRequest, slug: str, page_slug: str = "index") -> Http "submitted_slug": request.POST.get("slug", page_slug), "is_new": not doc.exists, "form_title": "Edit or rename page" if doc.exists else "New page", + "saved_files": saved_files, + "attachment_error": attachment_error, }, ) diff --git a/coulomb_social/templates/spaces/attachments.html b/coulomb_social/templates/spaces/attachments.html new file mode 100644 index 0000000..5e74aa3 --- /dev/null +++ b/coulomb_social/templates/spaces/attachments.html @@ -0,0 +1,49 @@ +{% extends "base.html" %} +{% block title %}Attachments — {{ doc.title }}{% endblock %} +{% block content %} +

← {{ doc.title }}

+

Attachments

+

Files for {{ doc.title }}. Access follows this space’s sharing settings.

+
+
    + {% for file in files %} +
  • + {{ file.name }} + {% if file.open_url %}Open image{% endif %} + Download + {% if can_write %} + {% if file.managed %} +
    + {% csrf_token %} + + + + +
    + {% else %} + Edit page reference + {% endif %} + {% endif %} +
  • + {% empty %} +
  • No attachments on this page.
  • + {% endfor %} +
+
+{% if can_write %} +
+

Add attachment

+
+ {% csrf_token %} + + +


+

+ +
+

Removing a file from this page keeps it in earlier revisions. Existing links and visuals can be changed in the page editor.

+
+{% else %} +

Read-only access. Authors can add or remove attachments.

+{% endif %} +{% endblock %} diff --git a/coulomb_social/templates/spaces/detail.html b/coulomb_social/templates/spaces/detail.html index 14f3571..b61e246 100644 --- a/coulomb_social/templates/spaces/detail.html +++ b/coulomb_social/templates/spaces/detail.html @@ -109,6 +109,12 @@ title="Copy the page content to paste into an email" aria-controls="copy-content-status"> Copy content to clipboard + {% if page.source == 'content-plane' %} +
+ Attachments +

{% if can_write %}Open, download or manage attachments{% else %}Open or download attachments{% endif %}

+
+ {% endif %} {% endif %} {% if can_write %} Edit page diff --git a/coulomb_social/templates/spaces/page_form.html b/coulomb_social/templates/spaces/page_form.html index 349e8c5..526373b 100644 --- a/coulomb_social/templates/spaces/page_form.html +++ b/coulomb_social/templates/spaces/page_form.html @@ -32,6 +32,30 @@

{{ form_title }}

Change the title to rename the displayed protocol. Change the file name below to rename its Markdown file and page address.

+ {% if not is_new %} +
+

Attachments

+

Files on the saved page. Editing or renaming the page keeps its attachments.

+ {% if attachment_error %} +

{{ attachment_error }}

+ {% else %} +
    + {% for file in saved_files %} +
  • + {{ file.name }} + {% if file.open_url %}Open image{% endif %} + Download +
  • + {% empty %} +
  • No attachments on this page.
  • + {% endfor %} +
+ {% endif %} + Manage attachments (new tab) +

Save your edits before adding or removing files, then reopen the editor.

+
+ {% endif %} +
{% csrf_token %} diff --git a/docs/migration/bubble-auth-and-export.md b/docs/migration/bubble-auth-and-export.md index b40deae..eb47e7a 100644 --- a/docs/migration/bubble-auth-and-export.md +++ b/docs/migration/bubble-auth-and-export.md @@ -142,3 +142,101 @@ Set COULOMB_SOCIAL_CORPUS_DIR to a private directory if needed. Do not put raw exports in the checkout. Default target route is /vw_pages; override via COULOMB_SOCIAL_CAPTURE_ROUTE only after confirming the actual authenticated protocol surface. A successful public-route load is not authentication proof. + + +## Retained Visual capture and review bundle + +The October31 all-space bundle now has a separate private Visual preparation at +`~/.config/coulomb-social/auth/exports/visual-bundle-20261031`. The original +`review-bundle-20261031` remains unchanged.242Visual references were captured from +108URLs on the one recorded Bubble CDN, producing90distinct images.236draft +article pages and67space indexes link local hashed assets. All90images decode in +an offline browser; this is not a source/product appearance verdict or an import. + +The reusable collector is `scripts/capture-bubble-visuals.py ORIGINAL --output NEW`. +It requires a complete hash-bound original bundle, a new private directory outside +repositories, and the exact known CDN over HTTPS. It refuses redirects, alternate +origins, credentials in URLs, active/unknown image formats, source drift, oversized +responses and existing output. No stored browser credentials are read or sent. +A missing final reconciliation.json means partial output, which must be retained +for review rather than used as an import bundle. Original body/date bytes are not +rewritten; only the compiler's empty Visual metadata is replaced. + +Verification for the completed bundle (read-only): + +```bash +python3 scripts/verify-bubble-visual-preservation.py \ + ~/.config/coulomb-social/auth/exports/review-bundle-20261031 \ + ~/.config/coulomb-social/auth/exports/visual-bundle-20261031 +node scripts/verify-bubble-visuals.mjs \ + ~/.config/coulomb-social/auth/exports/visual-bundle-20261031 +``` + +Only aggregate results belong in Git/chat. Images, titles, source URLs and original +records remain private. The new bundle retains all71spaces and the existing +graph/grant/fidelity gates.18body links and one attachment flag are still +unreconciled. Captured Visual bytes do not authorize copying memberships or +productive import. Public proof: docs/release/bubble-visual-preparation-2026-10-31.json. + +## Private access reconciliation + +`scripts/audit-bubble-access.py ORIGINAL NEW_PRIVATE_DIRECTORY` inventories the +retained source ownership, membership and visibility fields without proposing +target grants. It requires private, hash-bound source snapshots and refuses +source drift, symlinks, public files, hard links, existing output and output +inside the original bundle or this repository. The JSON and escaped, network-free +HTML review are private; stdout contains aggregate counts and hashes only. + +The October 31 review is at +`~/.config/coulomb-social/auth/exports/access-review-20261031/index.html`. +It covers 70 fresh spaces, 245 article records and one inventory-only space. +Three distinct source account references remain to be linked through authoritative +identity evidence. Nine creator references are unresolved. A missing source +membership field is unknown, not an empty list. Source role names and visibility +values are preserved for review; no equivalence with target owner/member/viewer +permissions is assumed. The accepted ReichelAG owner grant remains evidence for +that cohort alone. See `docs/release/bubble-access-audit-2026-10-31.json`. + +## Body links and deferred scrap relations + +`scripts/verify-bubble-body-resources.py ORIGINAL VISUAL --output PRIVATE_JSON` +checks source hashes, recompiles original draft bytes, checks prepared bodies and +compares reference attributes before and after the product sanitizer. It neither +fetches destinations nor changes source files. Its stdout receipt contains only +aggregate counts and hashes. Raw references and deferred scrap descriptors belong +in the separate private report, outside both bundles and the checkout. + +The October 31 proof verifies 18 distinct body-link entries with 28 occurrences, +all preserved by the product renderer. It also verifies 242 source bodies and 236 +prepared pages. This does not prove remote availability or Bubble appearance. +Four `_list_custom_scrap` values are deferred strings, not captured record lists; +the compiler now preserves them as unresolved review flags instead of silently +omitting them. Explicit empty lists remain distinct from missing or deferred data. +These four records and the separate attachment-flagged article still need source +recovery or disposition. Do not interpret the strings as article IDs or infer +that the attachment flag enumerates all resources. The supplemental report is +`~/.config/coulomb-social/auth/exports/body-resources-20261031.json`; the existing +original and Visual bundle receipts remain unchanged. Public proof: +`docs/release/bubble-body-resources-2026-10-31.json`. + +### Native resource follow-up + +The separate `resources-20261031` private directory records a bounded source +observation of the five affected articles. Their fresh parent records match the +retained capture, but the page visits returned no typed scrap/attachment records. +Two response reads failed, so zero captured records must not be read as zero +existing resources. No prior capture was overwritten. The private `index.html` +links all five original pages for the operator to identify the existing-resource +view/download path. Add attachment controls were observed but not activated. + +The collector is `scripts/capture-bubble-resources.mjs`, taking `--articles`, +`--expect-articles-sha256` and a new private `--output` directory. It visits at +most ten explicit affected article IDs, selects only typed scrap/attachment +content envelopes from the existing same-origin read responses, and excludes +user/session envelopes. Multiple observed resource versions remain separate; +empty initialization envelopes remain unresolved. It does not decode or invent +provider query payloads, click controls, submit forms, fetch arbitrary attachment +URLs or create an import manifest. `completed_bounded_observation` describes only +the terminal visit pass; completeness remains false. Source hashes, response +failures and private checkpoint hashes are in +`docs/release/bubble-resource-capture-2026-10-31.json`. diff --git a/docs/release/attachment-menu-validation-2026-10-31.json b/docs/release/attachment-menu-validation-2026-10-31.json new file mode 100644 index 0000000..82c4af5 --- /dev/null +++ b/docs/release/attachment-menu-validation-2026-10-31.json @@ -0,0 +1,32 @@ +{ + "schema": "coulomb.attachment-menu-source-validation.v1", + "product_decision": "78175c34-58e1-4ad6-944d-0662c0462ae6", + "full_main_gate": { + "command": "TMPDIR=/var/tmp make check", + "passed": 670, + "failures": 0, + "errors": 0, + "skipped": 0, + "log_sha256": "357547d34f7d73f750721338cff3e07439e8c710d0f1cf838541c1b175268335" + }, + "isolated_release_gate": { + "source": "1139a6d30eee4f75317b46bd45120a15d818455f", + "command": "make check", + "passed": 568, + "failures": 0, + "errors": 0, + "skipped": 0, + "log_sha256": "d9a9c8883f0d4021918c8600f9b5055cac24ca6b9943ee89db067f55d65ccdb1" + }, + "checks": [ + "Author upload/remove; authorized reader download; foreign-tenant, outsider, revoked, archived and anonymous denial", + "Stale page and CSRF refusal; immutable file names; 16 MiB limit; traversal and symlink denial", + "Retained body/source dates and associations through edit/rename; removal keeps historical revision bytes", + "Independent copy/move associations and managed files through 14 abrupt process deaths", + "Upload recovery at partial bytes, complete bytes, stored revision and published page", + "Real loopback browser upload/download/copy/move/remove and permission checks; no external requests" + ], + "synthetic_fixtures_only": true, + "production_deployed": false, + "productive_user_acceptance": false +} diff --git a/docs/release/backup-completed-evidence-custody-return-2026-10-31.json b/docs/release/backup-completed-evidence-custody-return-2026-10-31.json new file mode 100644 index 0000000..d44ab55 --- /dev/null +++ b/docs/release/backup-completed-evidence-custody-return-2026-10-31.json @@ -0,0 +1,280 @@ +{ + "schema": "coulomb.completed-evidence-custody-independent-return.v1", + "verified_at": "2026-10-31T16:37:46.139987+00:00", + "proposal_digest": "sha256:fa2763d1dcbe92fe638a29c9a7a7d51c03d30849c4232f2d43f81ef705bae611", + "receipt_sha256": "b79b26b1572faff0e2f2257e39e438bc065a4d2c71f92d879749e0e8b4b03493", + "receipt": { + "bytes_modified": false, + "capture_executed": false, + "changes": [ + { + "after": { + "bytes": 4096, + "device": 2049, + "gid": 0, + "hash_kind": "directory-entry-names", + "inode": 543834, + "kind": "directory", + "mode": "0o700", + "mtime_ns": 1793430825511133103, + "nlink": 4, + "relative_path": "releases", + "sha256": "3190439b978096a16c28aa0dd6b372735594808217b59a01dadbbe744d4110c8", + "uid": 0 + }, + "before": { + "bytes": 4096, + "device": 2049, + "gid": 0, + "hash_kind": "directory-entry-names", + "inode": 543834, + "kind": "directory", + "mode": "0o755", + "mtime_ns": 1793430825511133103, + "nlink": 4, + "relative_path": "releases", + "sha256": "3190439b978096a16c28aa0dd6b372735594808217b59a01dadbbe744d4110c8", + "uid": 0 + }, + "path": "releases" + }, + { + "after": { + "bytes": 4096, + "device": 2049, + "gid": 0, + "hash_kind": "directory-entry-names", + "inode": 549330, + "kind": "directory", + "mode": "0o700", + "mtime_ns": 1793430825511133103, + "nlink": 3, + "relative_path": "releases/9f1daf6c5fb56b2b4fc9817f28f5ad090c884b0295f7d6f4469e42d48fa8d59f/capsule/charts", + "sha256": "6b8295661773cb5820c305091679383b09fb0d7dcdde4412eeae48bfabab28b5", + "uid": 0 + }, + "before": { + "bytes": 4096, + "device": 2049, + "gid": 0, + "hash_kind": "directory-entry-names", + "inode": 549330, + "kind": "directory", + "mode": "0o755", + "mtime_ns": 1793430825511133103, + "nlink": 3, + "relative_path": "releases/9f1daf6c5fb56b2b4fc9817f28f5ad090c884b0295f7d6f4469e42d48fa8d59f/capsule/charts", + "sha256": "6b8295661773cb5820c305091679383b09fb0d7dcdde4412eeae48bfabab28b5", + "uid": 0 + }, + "path": "releases/9f1daf6c5fb56b2b4fc9817f28f5ad090c884b0295f7d6f4469e42d48fa8d59f/capsule/charts" + }, + { + "after": { + "bytes": 4096, + "device": 2049, + "gid": 0, + "hash_kind": "directory-entry-names", + "inode": 549341, + "kind": "directory", + "mode": "0o700", + "mtime_ns": 1793430825512133101, + "nlink": 3, + "relative_path": "releases/9f1daf6c5fb56b2b4fc9817f28f5ad090c884b0295f7d6f4469e42d48fa8d59f/capsule/release", + "sha256": "25a35515bc386a0c0a1a91b5fa3c966fffad64e2ce1d8bde35959bc164b11d79", + "uid": 0 + }, + "before": { + "bytes": 4096, + "device": 2049, + "gid": 0, + "hash_kind": "directory-entry-names", + "inode": 549341, + "kind": "directory", + "mode": "0o755", + "mtime_ns": 1793430825512133101, + "nlink": 3, + "relative_path": "releases/9f1daf6c5fb56b2b4fc9817f28f5ad090c884b0295f7d6f4469e42d48fa8d59f/capsule/release", + "sha256": "25a35515bc386a0c0a1a91b5fa3c966fffad64e2ce1d8bde35959bc164b11d79", + "uid": 0 + }, + "path": "releases/9f1daf6c5fb56b2b4fc9817f28f5ad090c884b0295f7d6f4469e42d48fa8d59f/capsule/release" + }, + { + "after": { + "bytes": 4096, + "device": 2049, + "gid": 0, + "hash_kind": "directory-entry-names", + "inode": 549219, + "kind": "directory", + "mode": "0o700", + "mtime_ns": 1793406468579820277, + "nlink": 3, + "relative_path": "releases/5fd7ccd8f68057346367d81efb82b48280a16dd31d87683a5bd83e60e3d1837e/capsule/charts", + "sha256": "6b8295661773cb5820c305091679383b09fb0d7dcdde4412eeae48bfabab28b5", + "uid": 0 + }, + "before": { + "bytes": 4096, + "device": 2049, + "gid": 0, + "hash_kind": "directory-entry-names", + "inode": 549219, + "kind": "directory", + "mode": "0o755", + "mtime_ns": 1793406468579820277, + "nlink": 3, + "relative_path": "releases/5fd7ccd8f68057346367d81efb82b48280a16dd31d87683a5bd83e60e3d1837e/capsule/charts", + "sha256": "6b8295661773cb5820c305091679383b09fb0d7dcdde4412eeae48bfabab28b5", + "uid": 0 + }, + "path": "releases/5fd7ccd8f68057346367d81efb82b48280a16dd31d87683a5bd83e60e3d1837e/capsule/charts" + }, + { + "after": { + "bytes": 4096, + "device": 2049, + "gid": 0, + "hash_kind": "directory-entry-names", + "inode": 549263, + "kind": "directory", + "mode": "0o700", + "mtime_ns": 1793412565567095985, + "nlink": 2, + "relative_path": "date-operations/47829c7ac2e6fd2c81fde1c914aec7c6e433098fa66985db6cfb9b34069942dc/private", + "sha256": "384eaf76c1a05ea25236c7a0d7bee390638b68fc89186604def37028364992fb", + "uid": 0 + }, + "before": { + "bytes": 4096, + "device": 2049, + "gid": 10001, + "hash_kind": "directory-entry-names", + "inode": 549263, + "kind": "directory", + "mode": "0o700", + "mtime_ns": 1793412565567095985, + "nlink": 2, + "relative_path": "date-operations/47829c7ac2e6fd2c81fde1c914aec7c6e433098fa66985db6cfb9b34069942dc/private", + "sha256": "384eaf76c1a05ea25236c7a0d7bee390638b68fc89186604def37028364992fb", + "uid": 10001 + }, + "path": "date-operations/47829c7ac2e6fd2c81fde1c914aec7c6e433098fa66985db6cfb9b34069942dc/private" + }, + { + "after": { + "bytes": 455, + "device": 2049, + "gid": 0, + "hash_kind": "file-bytes", + "inode": 549311, + "kind": "regular", + "mode": "0o600", + "mtime_ns": 1793412565564096058, + "nlink": 1, + "relative_path": "date-operations/47829c7ac2e6fd2c81fde1c914aec7c6e433098fa66985db6cfb9b34069942dc/private/result.json", + "sha256": "859e5a80442ad77b5eff61b68e5062a167d51f5db1b6c2d1ee064db977beb022", + "uid": 0 + }, + "before": { + "bytes": 455, + "device": 2049, + "gid": 10001, + "hash_kind": "file-bytes", + "inode": 549311, + "kind": "regular", + "mode": "0o600", + "mtime_ns": 1793412565564096058, + "nlink": 1, + "relative_path": "date-operations/47829c7ac2e6fd2c81fde1c914aec7c6e433098fa66985db6cfb9b34069942dc/private/result.json", + "sha256": "859e5a80442ad77b5eff61b68e5062a167d51f5db1b6c2d1ee064db977beb022", + "uid": 10001 + }, + "path": "date-operations/47829c7ac2e6fd2c81fde1c914aec7c6e433098fa66985db6cfb9b34069942dc/private/result.json" + }, + { + "after": { + "bytes": 12930, + "device": 2049, + "gid": 0, + "hash_kind": "file-bytes", + "inode": 549315, + "kind": "regular", + "mode": "0o600", + "mtime_ns": 1793412565062108322, + "nlink": 1, + "relative_path": "date-operations/47829c7ac2e6fd2c81fde1c914aec7c6e433098fa66985db6cfb9b34069942dc/private/journal.json", + "sha256": "fef62ad9317066d5d82eccbea0317eceb2f883e07f50935104e47113855fe0dd", + "uid": 0 + }, + "before": { + "bytes": 12930, + "device": 2049, + "gid": 10001, + "hash_kind": "file-bytes", + "inode": 549315, + "kind": "regular", + "mode": "0o600", + "mtime_ns": 1793412565062108322, + "nlink": 1, + "relative_path": "date-operations/47829c7ac2e6fd2c81fde1c914aec7c6e433098fa66985db6cfb9b34069942dc/private/journal.json", + "sha256": "fef62ad9317066d5d82eccbea0317eceb2f883e07f50935104e47113855fe0dd", + "uid": 10001 + }, + "path": "date-operations/47829c7ac2e6fd2c81fde1c914aec7c6e433098fa66985db6cfb9b34069942dc/private/journal.json" + }, + { + "after": { + "bytes": 0, + "device": 2049, + "gid": 0, + "hash_kind": "file-bytes", + "inode": 549310, + "kind": "regular", + "mode": "0o600", + "mtime_ns": 1793412563167160722, + "nlink": 1, + "relative_path": "date-operations/47829c7ac2e6fd2c81fde1c914aec7c6e433098fa66985db6cfb9b34069942dc/private/execute.lock", + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "uid": 0 + }, + "before": { + "bytes": 0, + "device": 2049, + "gid": 10001, + "hash_kind": "file-bytes", + "inode": 549310, + "kind": "regular", + "mode": "0o600", + "mtime_ns": 1793412563167160722, + "nlink": 1, + "relative_path": "date-operations/47829c7ac2e6fd2c81fde1c914aec7c6e433098fa66985db6cfb9b34069942dc/private/execute.lock", + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "uid": 10001 + }, + "path": "date-operations/47829c7ac2e6fd2c81fde1c914aec7c6e433098fa66985db6cfb9b34069942dc/private/execute.lock" + } + ], + "data_deleted": false, + "paths_moved": false, + "postflight": { + "historical_operations_still_terminal": true, + "state_bytes": 11259904, + "whole_state_custody_guard_passed": true + }, + "proposal_digest": "sha256:fa2763d1dcbe92fe638a29c9a7a7d51c03d30849c4232f2d43f81ef705bae611", + "schema": "coulomb.completed-evidence-custody.v1", + "started_at": "2026-10-31T16:36:38.149305+00:00", + "status": "completed_evidence_private_root_custody_verified", + "timers_activated": false, + "updated_at": "2026-10-31T16:36:38.939513+00:00", + "verified_entries": 8 + }, + "native_entries_verified": 8, + "all_historical_hashes_inodes_paths_mtimes_preserved": true, + "whole_state_custody_guard_passed": true, + "state_bytes": 11259904, + "verification_read_only": true, + "verifier_source_sha256": "27fb56bb621ab9141f6b7ae205790e8a7eeb1618e2628940b3b6bc5525b4393d" +} diff --git a/docs/release/backup-completed-evidence-custody-review-2026-10-31.md b/docs/release/backup-completed-evidence-custody-review-2026-10-31.md index 651eaeb..11cc589 100644 --- a/docs/release/backup-completed-evidence-custody-review-2026-10-31.md +++ b/docs/release/backup-completed-evidence-custody-review-2026-10-31.md @@ -1,11 +1,19 @@ # Completed backup evidence custody repair This is the exact proposed repair for the eight historical metadata exceptions -recorded in CSOC-WP-0012-T04. Native preflight passes. **Nothing has been changed.** +recorded in CSOC-WP-0012-T04. **Founder execution and independent postflight are +complete, under T15. Do not rerun the seal command.** Proposal: `sha256:fa2763d1dcbe92fe638a29c9a7a7d51c03d30849c4232f2d43f81ef705bae611`. +State Hub decision `3ec2f98e-9db3-47d9-bc1a-0387f980b81c` is resolved from the +founder's actual execution at16:36:38Z. Independent read-only verification at +16:37:46Z confirms all eight intended deltas and unchanged hashes, paths, inodes +and mtimes. The whole-state custody guard passes at11,259,904bytes. Evidence: +`backup-completed-evidence-custody-return-2026-10-31.json`. +`operator-handoff-check-2026-10-31.json` retains the earlier pre-execution check. + ## Exact change Within `/var/lib/coulomb-social-backup/`, change: @@ -33,9 +41,9 @@ The strict backup capacity guard is unchanged. Successful execution also runs it existing state-usage/custody check over the entire state tree. This is neither a blanket chmod/chown nor an exception that omits historical data from accounting. -## Founder terminal command +## Historical founder terminal command — completed -Run this from the workstation after reviewing the scope above: +Retained for provenance; do not run again: ```bash python3 /home/worsch/coulomb-social/scripts/seal-backup-evidence.py \ @@ -48,9 +56,9 @@ At the prompt, enter: SEAL sha256:fa2763d1dcbe92fe638a29c9a7a7d51c03d30849c4232f2d43f81ef705bae611 ``` -Expected status: `completed_evidence_private_root_custody_verified`, eight entries -verified and `postflight.whole_state_custody_guard_passed: true`. Paste the result -back for independent readback. The private durable receipt is: +Recorded status: `completed_evidence_private_root_custody_verified`, eight entries +verified and `postflight.whole_state_custody_guard_passed: true`. Independent +readback is complete. The private durable receipt is: `/var/lib/coulomb-social-backup-custody/fa2763d1dcbe92fe638a29c9a7a7d51c03d30849c4232f2d43f81ef705bae611/receipt.json`. @@ -106,12 +114,12 @@ Nineteen dedicated cases exercise real metadata/lock IO, preserved bytes/paths, explicit/partial rollback, interference refusal, source/digest boundaries, nonterminal refusal, and native receipt/process/helper/mount/cluster/timer guards. -This resolves only the current custody mismatch after attended success. T04 still +The verified attended execution resolves the historical custody mismatch. T04 still owns retention/capacity admission and historical-copy disposition. Preventing future release/date producers from reintroducing these metadata mismatches is also -live T04 work before recurring service acceptance: nested release directory -creation currently uses default intermediate modes, and date helper journals -retain the execution UID after completion. Do not silently change frozen capsules. +required before recurring service acceptance. That source correction is now +completed under T13/RAPPS-WP-0015-T06; it does not repair the historical entries +listed here or change frozen capsules. T07 still needs the existing route execution; T08 then needs inactive installation and independent readback. No native backup-operation or migration acceptance is inferred from this repair. diff --git a/docs/release/backup-installation-independent-return-2026-10-31.json b/docs/release/backup-installation-independent-return-2026-10-31.json new file mode 100644 index 0000000..8417e00 --- /dev/null +++ b/docs/release/backup-installation-independent-return-2026-10-31.json @@ -0,0 +1,371 @@ +{ + "schema": "coulomb.backup-installation-independent-postflight.v1", + "verified_at": "2026-10-31T17:30:18.219708+00:00", + "status": "installed_files_and_inactive_units_verified", + "proposal_digest": "sha256:8f442bf0ebed8bbf3b7f227b52081df52613f3f5f6874c4a59511cfc2ba06d42", + "receipt_sha256": "96329afbd5662f71bf6d6b05c003f0810be12c10036f9cadb41cc373d3a1108b", + "receipt": { + "schema": "coulomb.backup-source-supervised-installation.v1", + "proposal_digest": "sha256:8f442bf0ebed8bbf3b7f227b52081df52613f3f5f6874c4a59511cfc2ba06d42", + "status": "sources_installed_units_inactive_verified", + "preflight": { + "units": { + "coulomb-social-paired-backup.service": { + "LoadState": "not-found", + "ActiveState": "inactive", + "SubState": "dead", + "FragmentPath": "", + "UnitFileState": "" + }, + "coulomb-social-paired-backup.timer": { + "LoadState": "not-found", + "ActiveState": "inactive", + "SubState": "dead", + "FragmentPath": "", + "UnitFileState": "" + }, + "coulomb-social-backup-freshness.service": { + "LoadState": "not-found", + "ActiveState": "inactive", + "SubState": "dead", + "FragmentPath": "", + "UnitFileState": "" + }, + "coulomb-social-backup-freshness.timer": { + "LoadState": "not-found", + "ActiveState": "inactive", + "SubState": "dead", + "FragmentPath": "", + "UnitFileState": "" + }, + "coulomb-social-backup-alert@coulomb-social-paired-backup.service.service": { + "LoadState": "not-found", + "ActiveState": "inactive", + "SubState": "dead", + "FragmentPath": "", + "UnitFileState": "" + } + }, + "capacity": { + "state_bytes": 11259904, + "state_reserve_bytes": 7532969984, + "disk_reserve_bytes": 8606711808, + "free_bytes": 115579949056, + "free_inodes": 24523113, + "filesystem_blocks_reserved": false + }, + "sdk": { + "boto3": "1.40.18", + "botocore": "1.40.76" + }, + "route": { + "helm_revision": 6, + "runtime_and_complete_resources_verified": true + } + }, + "timers_activated": false, + "capture_executed": false, + "email_sent": false, + "retained_data_deleted": false, + "started_at": "2026-10-31T17:22:48.757530+00:00", + "capacity_profile": { + "maximum_new_file_bytes": 1073741824, + "state_budget_bytes": 8589934592, + "minimum_free_bytes": 8589934592, + "minimum_free_inodes": 1024, + "control_margin_bytes": 16777216 + }, + "founder_confirmed_profile_and_installation": true, + "route_postflight": { + "helm_revision": 6, + "runtime_and_complete_resources_verified": true + }, + "verified_targets": 19, + "units": { + "coulomb-social-paired-backup.service": { + "LoadState": "loaded", + "ActiveState": "inactive", + "SubState": "dead", + "FragmentPath": "/etc/systemd/system/coulomb-social-paired-backup.service", + "UnitFileState": "static" + }, + "coulomb-social-paired-backup.timer": { + "LoadState": "loaded", + "ActiveState": "inactive", + "SubState": "dead", + "FragmentPath": "/etc/systemd/system/coulomb-social-paired-backup.timer", + "UnitFileState": "disabled" + }, + "coulomb-social-backup-freshness.service": { + "LoadState": "loaded", + "ActiveState": "inactive", + "SubState": "dead", + "FragmentPath": "/etc/systemd/system/coulomb-social-backup-freshness.service", + "UnitFileState": "static" + }, + "coulomb-social-backup-freshness.timer": { + "LoadState": "loaded", + "ActiveState": "inactive", + "SubState": "dead", + "FragmentPath": "/etc/systemd/system/coulomb-social-backup-freshness.timer", + "UnitFileState": "disabled" + }, + "coulomb-social-backup-alert@coulomb-social-paired-backup.service.service": { + "LoadState": "loaded", + "ActiveState": "inactive", + "SubState": "dead", + "FragmentPath": "/etc/systemd/system/coulomb-social-backup-alert@.service", + "UnitFileState": "static" + } + }, + "updated_at": "2026-10-31T17:22:54.560675+00:00" + }, + "nested_receipt_sha256": "795d9eb672b24495c77ad636a210c5d2022c8c3124d276920db7eda02257e519", + "verified_targets": 19, + "installed_fingerprints": { + "/opt/coulomb-social-backup/paired-backup.py": { + "exists": true, + "sha256": "d161c742d1a6a182460a57d0a1719591ab58fc94006f55169ad3d74442a93630", + "mode": 384, + "uid": 0, + "gid": 0, + "inode": 534009, + "device": 2049, + "mtime_ns": 1793467368826077309 + }, + "/opt/coulomb-social-backup/snapshot-content-and-db.py": { + "exists": true, + "sha256": "dcc8dd2a3989f279597595f9260b31b24fcc9b6dc787f04c17ef0491c53d830d", + "mode": 384, + "uid": 0, + "gid": 0, + "inode": 549255, + "device": 2049, + "mtime_ns": 1793430923413704430 + }, + "/opt/coulomb-social-backup/mirror-paired-backup.py": { + "exists": true, + "sha256": "9ceb26d9083af016c4b787907e4dce7bdf437c782fb2d15eb28d9e334e6e431c", + "mode": 384, + "uid": 0, + "gid": 0, + "inode": 549201, + "device": 2049, + "mtime_ns": 1793467368831077393 + }, + "/opt/coulomb-social-backup/check-paired-backup-health.py": { + "exists": true, + "sha256": "2d40ec6e4c59ea7fe1d46065d197b9a87ca6811f023cc6f3e20e4184a3490566", + "mode": 384, + "uid": 0, + "gid": 0, + "inode": 549309, + "device": 2049, + "mtime_ns": 1793467368836077477 + }, + "/opt/coulomb-social-backup/backup_capacity.py": { + "exists": true, + "sha256": "cb699a79e67b129239b5aa5d6f3b0d4ffaa9773a8b073227ad17fdecb5b9d893", + "mode": 384, + "uid": 0, + "gid": 0, + "inode": 549354, + "device": 2049, + "mtime_ns": 1793467368843077594 + }, + "/opt/coulomb-social-backup/mirror-platform-runtime.json": { + "exists": true, + "sha256": "9253c5f6e956b67f499d3e12d2851be7fabd3bc56b00496b8810bbd0be8209f1", + "mode": 384, + "uid": 0, + "gid": 0, + "inode": 549355, + "device": 2049, + "mtime_ns": 1793467368847077662 + }, + "/opt/coulomb-social-backup/backup_alerts.py": { + "exists": true, + "sha256": "07522437d4276b92f0a44149adc6c9e81bb18dbe59ce0f1f9e58a48347700d0e", + "mode": 384, + "uid": 0, + "gid": 0, + "inode": 549359, + "device": 2049, + "mtime_ns": 1793467368887078332 + }, + "/opt/coulomb-social-backup/notify-backup-alert.py": { + "exists": true, + "sha256": "0bd53bc5ad22d8bcb0326fb18a17737744b06416fe785dad077308e45f051d5a", + "mode": 384, + "uid": 0, + "gid": 0, + "inode": 549360, + "device": 2049, + "mtime_ns": 1793467368891078399 + }, + "/opt/coulomb-social-backup/observe-backup-alerts.py": { + "exists": true, + "sha256": "17caf4fa65283242f618b039ff1d018801c9890ea2a9178912cbdd4b50ee2664", + "mode": 384, + "uid": 0, + "gid": 0, + "inode": 549361, + "device": 2049, + "mtime_ns": 1793467368895078466 + }, + "/opt/railiance-platform/scripts/application_recovery_identities.py": { + "exists": true, + "sha256": "20f56255ed6eeec4aafe33f142bc3ee03f623d4957a7b0cabb741d340f15f79d", + "mode": 384, + "uid": 0, + "gid": 0, + "inode": 549356, + "device": 2049, + "mtime_ns": 1793467368852077745 + }, + "/opt/railiance-platform/scripts/application_recovery_mirror.py": { + "exists": true, + "sha256": "d48ab95e89b9eeae36269d853ebe4c44c22e1f57d4e14224f03d0b9aeeb34178", + "mode": 384, + "uid": 0, + "gid": 0, + "inode": 549357, + "device": 2049, + "mtime_ns": 1793467368857077829 + }, + "/opt/railiance-platform/scripts/application_recovery_resources.py": { + "exists": true, + "sha256": "ab70ec341e738272730eb0daf76840d0bca807caeb3c4e4c2357f162fbed4498", + "mode": 384, + "uid": 0, + "gid": 0, + "inode": 549358, + "device": 2049, + "mtime_ns": 1793467368862077913 + }, + "/opt/railiance-platform/scripts/primary_application_archive.py": { + "exists": true, + "sha256": "a3f0a61e869a6012edb4b5288be14e3b0c4a9a1273b090320ab146d12e5aa94d", + "mode": 420, + "uid": 0, + "gid": 0, + "inode": 540610, + "device": 2049, + "mtime_ns": 1791537447437616251 + }, + "/etc/systemd/system/coulomb-social-backup-freshness.service": { + "exists": true, + "sha256": "8d6278c2cbca7f48b659c239c1d1e181fa4b9cf023fe7429850e294c6cd84d0e", + "mode": 420, + "uid": 0, + "gid": 0, + "inode": 19366, + "device": 2049, + "mtime_ns": 1793467368868078014 + }, + "/etc/systemd/system/coulomb-social-paired-backup.service": { + "exists": true, + "sha256": "8b01b788bb08f453fdd4807104185c89411e0b0b4dedee9240f14f110ceb5513", + "mode": 420, + "uid": 0, + "gid": 0, + "inode": 14867, + "device": 2049, + "mtime_ns": 1793467368873078098 + }, + "/etc/systemd/system/coulomb-social-backup-freshness.timer": { + "exists": true, + "sha256": "75d1d0721c07fdd0703910c06bb8a678396440e7d95411a37ee7de4938f3ac31", + "mode": 420, + "uid": 0, + "gid": 0, + "inode": 19367, + "device": 2049, + "mtime_ns": 1793467368878078181 + }, + "/etc/systemd/system/coulomb-social-paired-backup.timer": { + "exists": true, + "sha256": "5e87235e66f89a64a972562d6f163f62420628fc44fb7fd8d603a96c347dfbb9", + "mode": 420, + "uid": 0, + "gid": 0, + "inode": 19368, + "device": 2049, + "mtime_ns": 1793467368882078248 + }, + "/etc/systemd/system/coulomb-social-backup-alert@.service": { + "exists": true, + "sha256": "4bfa019d29b1d0de278f03553602a5e824de50ed99ebcf3b5035cf4d5b06fe98", + "mode": 420, + "uid": 0, + "gid": 0, + "inode": 19370, + "device": 2049, + "mtime_ns": 1793467368899078533 + }, + "/etc/coulomb-social-backup/alerting.env": { + "exists": true, + "sha256": "509844b5c8ef618b812d52f32e19c3c559623636dca62f7b16f437aa90cc0458", + "mode": 384, + "uid": 0, + "gid": 0, + "inode": 19369, + "device": 2049, + "mtime_ns": 1793467368904078617 + } + }, + "units": { + "coulomb-social-paired-backup.service": { + "LoadState": "loaded", + "ActiveState": "inactive", + "SubState": "dead", + "FragmentPath": "/etc/systemd/system/coulomb-social-paired-backup.service", + "UnitFileState": "static" + }, + "coulomb-social-paired-backup.timer": { + "LoadState": "loaded", + "ActiveState": "inactive", + "SubState": "dead", + "FragmentPath": "/etc/systemd/system/coulomb-social-paired-backup.timer", + "UnitFileState": "disabled" + }, + "coulomb-social-backup-freshness.service": { + "LoadState": "loaded", + "ActiveState": "inactive", + "SubState": "dead", + "FragmentPath": "/etc/systemd/system/coulomb-social-backup-freshness.service", + "UnitFileState": "static" + }, + "coulomb-social-backup-freshness.timer": { + "LoadState": "loaded", + "ActiveState": "inactive", + "SubState": "dead", + "FragmentPath": "/etc/systemd/system/coulomb-social-backup-freshness.timer", + "UnitFileState": "disabled" + }, + "coulomb-social-backup-alert@coulomb-social-paired-backup.service.service": { + "LoadState": "loaded", + "ActiveState": "inactive", + "SubState": "dead", + "FragmentPath": "/etc/systemd/system/coulomb-social-backup-alert@.service", + "UnitFileState": "static" + } + }, + "route": { + "helm_revision": 6, + "runtime_and_complete_resources_verified": true + }, + "capacity": { + "state_bytes": 11259904, + "state_reserve_bytes": 7532969984, + "disk_reserve_bytes": 8606711808, + "free_bytes": 115555618816, + "free_inodes": 24523041, + "filesystem_blocks_reserved": false + }, + "sdk": { + "boto3": "1.40.18", + "botocore": "1.40.76" + }, + "runtime_modified_by_verification": false +} diff --git a/docs/release/backup-route-independent-return-2026-10-31.json b/docs/release/backup-route-independent-return-2026-10-31.json new file mode 100644 index 0000000..fb0a51f --- /dev/null +++ b/docs/release/backup-route-independent-return-2026-10-31.json @@ -0,0 +1,29 @@ +{ + "schema": "coulomb.backup-route-independent-postflight.v1", + "verified_at": "2026-10-31T17:19:36.926769+00:00", + "status": "durable_receipt_and_live_route_verified", + "execution_digest": "sha256:4d8481061dc9ce6835169defc96887945052332a0db8d4234ae84032d63cc328", + "receipt": { + "schema": "rapp-telemetry.backup-route-execution.v1", + "execution_digest": "sha256:4d8481061dc9ce6835169defc96887945052332a0db8d4234ae84032d63cc328", + "proposal_digest": "sha256:4bcad54f7b8e1e77b94112e452cbb58eaf8e882dc10305129c5f9ca69f014a66", + "status": "backup_email_route_deployed_verified", + "timers_activated": false, + "notification_sent_by_helper": false, + "production_data_restored": false, + "started_at": "2026-10-31T17:17:13.743344+00:00", + "helm_revision": 6, + "verified_resource_count": 87, + "after_resources_sha256": "600c4998e54b8e11845849c7d8e4c6b6699556c21f5df6ca79bb86b2e8d1683f", + "live_desired_fields_verified": true, + "runtime_config_verified": true, + "existing_image_and_pvc_preserved": true, + "completed_at": "2026-10-31T17:17:31.488654+00:00" + }, + "receipt_sha256": "c26d636f59109956f281ad46f4885c4db800a9553cf05cac324aa3155ef7ac40", + "original_execution_packet_matches": true, + "live_desired_fields_verified": true, + "runtime_config_verified": true, + "existing_image_and_pvc_preserved": true, + "runtime_modified_by_verification": false +} diff --git a/docs/release/backup-source-installation-execution-review-2026-10-31.md b/docs/release/backup-source-installation-execution-review-2026-10-31.md index b003566..f60c40a 100644 --- a/docs/release/backup-source-installation-execution-review-2026-10-31.md +++ b/docs/release/backup-source-installation-execution-review-2026-10-31.md @@ -3,7 +3,9 @@ The final wrapper is prepared for CSOC-WP-0012-T08. It installs the previously sealed22-member archive into its exact19 target paths, using the existing transaction library. It neither captures data nor starts/enables a service or -timer. Current native preflight **refuses**; do not execute this packet yet. +timer. The earlier custody refusal was resolved by verified T15 execution at +16:36:38Z. T07's route return and a fresh installer preflight remain required; +do not execute this packet yet. Proposal: `sha256:8f442bf0ebed8bbf3b7f227b52081df52613f3f5f6874c4a59511cfc2ba06d42`. @@ -105,7 +107,7 @@ corrected. The sender preparer's embedded remote Python AST is unchanged, and the send-free return-mail rehearsal passed again. Historical evidence keeps its original source hashes; the new validation binds the current formatted sources. -T08 remains progress until actual installation and independent inactive-state +T08 remains wait until actual installation and independent inactive-state readback. T09 still needs native service/failure/retry/stale/producer-death proof; T10 still needs activation and observation after its existing prerequisites. No full service acceptance, mail delivery or migration verdict follows from diff --git a/docs/release/bubble-access-audit-2026-10-31.json b/docs/release/bubble-access-audit-2026-10-31.json new file mode 100644 index 0000000..ff0ce96 --- /dev/null +++ b/docs/release/bubble-access-audit-2026-10-31.json @@ -0,0 +1,131 @@ +{ + "schema": "coulomb.bubble-access-audit-result.v1", + "spaces": { + "records": 70, + "field_states": { + "Created By": { + "unresolved_reference": 5, + "observed": 64, + "missing": 1 + }, + "crew_list_user": { + "observed": 69, + "missing": 1 + }, + "members_list_user": { + "observed": 47, + "missing": 23 + }, + "guests_list_user": { + "missing": 69, + "observed": 1 + } + }, + "option_fields": { + "isprivate_boolean": { + "present": 69, + "missing": 1, + "distinct_observed_values": 2 + }, + "addnewusersasmember_boolean": { + "present": 20, + "missing": 50, + "distinct_observed_values": 2 + }, + "addnewusersasguest_boolean": { + "present": 21, + "missing": 49, + "distinct_observed_values": 2 + } + } + }, + "articles": { + "records": 245, + "field_states": { + "Created By": { + "observed": 239, + "unresolved_reference": 4, + "missing": 2 + }, + "_creator_user": { + "missing": 10, + "observed": 235 + }, + "_owner_user": { + "observed": 243, + "missing": 2 + }, + "moders_list_user": { + "missing": 71, + "observed": 174 + } + }, + "option_fields": { + "access_option_visibility": { + "present": 243, + "missing": 2, + "distinct_observed_values": 2 + } + } + }, + "known_spaces": 71, + "fresh_space_records": 70, + "inventory_only_spaces": 1, + "distinct_source_principals": 3, + "target_identity_mappings_established": 0, + "target_grants_proposed": 0, + "production_changed": false, + "source_visibility_semantics_verified": false, + "source_sha256": { + "spaces": "ae20b0cfd1705f486c0b67ef48ac84b396f953cbcef09f9a8f07d07a66f1bf71", + "articles": "f33967c79c0f8a60a7d504f5d5e4082f8bc9e3c34f6a3160cb200fd380831d62", + "retained_spaces": "92038249ec2e4eed4c8d61ffcdbe030232cebc52e59ba005baee506a16930550" + }, + "source_report_sha256": "75fcd87d17ea0c5042e903efd60e65839da7716e5399ff431a345486ddd10f01", + "recorded_at": "2026-10-31T14:05:30.738728+00:00", + "workplan_task": "CSOC-WP-0014-T01", + "task_status": "progress", + "accepted_mapping_scope_evidence": "docs/release/reichelag-imported-2026-10-06.md", + "validation": { + "focused_tests_passed": 13, + "full_gate": "make check passed", + "junit": { + "tests": 583, + "errors": 0, + "failures": 0, + "skipped": 0 + }, + "source_snapshots_unchanged": true, + "private_output_hashes_verified": true, + "output_modes_verified": true + }, + "statehub_readback": { + "task_id": "0002882d-f156-5a5d-b524-d04793f4684d", + "status": "progress", + "access_audit_description_synced": true, + "fix_consistency_exit_code": 0, + "automation_errors": 0, + "assessment_failures": 0, + "warnings": 6 + }, + "output_files": [ + { + "path": "access-inventory.json", + "sha256": "d4f97ccc55867361048325f7227721a50a59bcf70ea1af4dfa3db48e8e703cb2" + }, + { + "path": "index.html", + "sha256": "6c175577f8a34c7bda78f86461d68d2c2167708970517f417bc89d1fde37436b" + } + ], + "implementation_files": [ + { + "path": "scripts/audit-bubble-access.py", + "sha256": "6590a19e54b77521e31455944b085f92e784e2b85737579cb14caaeb8538807b" + }, + { + "path": "tests/test_bubble_access_audit.py", + "sha256": "392a799d62828c7508a1b25133d9c1c8de01959b7f1fec90ae737a7cefce7883" + } + ] +} diff --git a/docs/release/bubble-attachment-recovery-2026-10-31.json b/docs/release/bubble-attachment-recovery-2026-10-31.json new file mode 100644 index 0000000..04ec264 --- /dev/null +++ b/docs/release/bubble-attachment-recovery-2026-10-31.json @@ -0,0 +1,41 @@ +{ + "schema": "coulomb.bubble-attachment-recovery.v1", + "completed_at": "2026-10-31T19:35:10.719868+00:00", + "source_articles_sha256": "f33967c79c0f8a60a7d504f5d5e4082f8bc9e3c34f6a3160cb200fd380831d62", + "capture_sha256": "995fa2558bdfab1035e538152948843977e66e096b286cc762b45a232b0c0052", + "visited_pages": 6, + "previously_flagged_pages": 5, + "attachment_relations": 2, + "retained_files": 2, + "retained_bytes": 17529, + "all_ui_and_independent_download_bytes_match": true, + "original_five_parent_bodies_and_dates_unchanged": true, + "source_attachment_dates_retained": true, + "mapped_draft_pages": 1, + "example_parents_outside_original_capture": 1, + "new_attachment_child_records": 2, + "four_scrap_relations_resolved": false, + "original_bundle_files_preserved_in_place": 739, + "draft_body_and_existing_metadata_unchanged": true, + "source_resource_blockers_removed": false, + "resource_completeness_verified": false, + "production_imported": false, + "source_mutation_controls_activated": false, + "private_review_path": "/home/worsch/.config/coulomb-social/auth/exports/attachment-bundle-20261031/attachments-index.html", + "preparation_manifest_sha256": "f61f4dc48c4dcc66aa967df1bbf274f1027630a4417a82d249d04bb6b6b73184", + "validation": { + "full_gate": "TMPDIR=/var/tmp make check", + "passed": 670, + "errors": 0, + "failures": 0, + "skipped": 0, + "synthetic_resource_guards": 7, + "full_gate_log_sha256": "6195e8cd91bb2dca72e240159a51d4ced15c979f89f3909c9495b4b8874f6876" + }, + "implementation_sha256": { + "scripts/capture-bubble-attachments.mjs": "cf8cba78d3a80f07f3448a50fb74d9502a01b84ff8be880c4d9e2a697af22812", + "scripts/bubble-resource-utils.mjs": "4f4e125f4d48ea5d8fb6421fb570d04a09fd2668e42c4c4857d4ac3a211e89b0", + "scripts/tests/bubble-resource-utils.test.mjs": "e434f56d9c586195f11ec1c642ef3f4da41d7cd2d4c01fc44f066bd3f76db192" + }, + "private_preparation_driver_sha256": "e74d9571a6962e095bd299f15c47aa7a19f3223183f722a98a34a10450c29861" +} diff --git a/docs/release/bubble-body-resources-2026-10-31.json b/docs/release/bubble-body-resources-2026-10-31.json new file mode 100644 index 0000000..273978d --- /dev/null +++ b/docs/release/bubble-body-resources-2026-10-31.json @@ -0,0 +1,98 @@ +{ + "schema": "coulomb.bubble-body-resource-proof.v1", + "source_report_sha256": "75fcd87d17ea0c5042e903efd60e65839da7716e5399ff431a345486ddd10f01", + "visual_report_sha256": "a255df2237d95588b21a5db059487ca0178d1df28c810ebaaf744c5e6491267a", + "source_sha256": { + "articles": "f33967c79c0f8a60a7d504f5d5e4082f8bc9e3c34f6a3160cb200fd380831d62", + "retained_spaces": "92038249ec2e4eed4c8d61ffcdbe030232cebc52e59ba005baee506a16930550", + "spaces": "ae20b0cfd1705f486c0b67ef48ac84b396f953cbcef09f9a8f07d07a66f1bf71" + }, + "captured_articles": 245, + "source_bodies_verified": 242, + "missing_bodies": 3, + "prepared_pages_verified": 236, + "original_draft_files_recompiled_unchanged": 545, + "body_reference_inventory_entries": 18, + "body_reference_records": 1, + "body_reference_classifications": { + "external": 18 + }, + "reference_occurrences_before": 28, + "reference_occurrences_after": 28, + "records_with_changed_reference_attributes": 0, + "unresolved_scrap_relation_records": 4, + "attachment_flag_records": 1, + "live_destinations_checked": false, + "original_bubble_render_compared": false, + "productive_fidelity_accepted": false, + "production_imported": false, + "network_requests": 0, + "private_report_sha256": "4caec573982dafc82e7228d9221c941f585522e235d64d1f6edac3b43913cf26", + "recorded_at": "2026-10-31T14:11:42.653868+00:00", + "tasks": [ + "CSOC-WP-0014-T01", + "CSOC-WP-0013-T02" + ], + "task_statuses_changed": false, + "implementation_files": [ + { + "path": "scripts/verify-bubble-body-resources.py", + "sha256": "8c9a56964ea59422830481d5aacb668e8589f6096ca5130fb6d541edc0ab4d8d" + }, + { + "path": "scripts/verify-bubble-visual-preservation.py", + "sha256": "9fbc212bd668126a0f6ce6a458b1ca3fba1e14d4b44438c0697f63dc34b5bc3d" + }, + { + "path": "coulomb_social/apps/spaces/rendering_audit.py", + "sha256": "093f293f6651a759d77ce7d444ba83fdfaead016b353a167693b2e36c5807894" + }, + { + "path": "coulomb_social/apps/spaces/bubble_bundle.py", + "sha256": "fa928e049ba1052b736b13edf78883b249735312c6b98486e5dd16d4f2c96858" + }, + { + "path": "tests/test_rendering_audit.py", + "sha256": "5e1aa72a322409a9576962c817d8714ee5eed042bbc38f22cb656a8319f52f67" + }, + { + "path": "tests/test_bubble_bundle.py", + "sha256": "2ad0453be2c1c1643d4ef6af2e40dc270f70f863522aa221d1d4bb018a4f1f59" + }, + { + "path": "tests/test_bubble_body_resources.py", + "sha256": "b3c420b62bceb253cd7228f3005b235c2d3bfd170136b960968311eb8fe48077" + } + ], + "validation": { + "focused_tests_passed": 24, + "full_gate": "make check passed", + "current_verifier_reproduces_private_report": true, + "all_original_draft_bytes_verified": 545, + "junit": { + "tests": 595, + "errors": 0, + "failures": 0, + "skipped": 0 + }, + "focused_result_source": "full acceptance gate JUnit" + }, + "statehub_readback": { + "tasks": [ + { + "task_id": "0002882d-f156-5a5d-b524-d04793f4684d", + "status": "progress", + "body_resource_evidence_synced": true + }, + { + "task_id": "8d06cdee-e0ce-5c3e-bf98-705deed0ee62", + "status": "progress", + "body_resource_evidence_synced": true + } + ], + "fix_consistency_exit_code": 0, + "automation_errors": 0, + "assessment_failures": 0, + "warnings": 7 + } +} diff --git a/docs/release/bubble-resource-capture-2026-10-31.json b/docs/release/bubble-resource-capture-2026-10-31.json new file mode 100644 index 0000000..e63640c --- /dev/null +++ b/docs/release/bubble-resource-capture-2026-10-31.json @@ -0,0 +1,102 @@ +{ + "schema": "coulomb.bubble-resource-observation-result.v1", + "recorded_at": "2026-10-31T14:35:31.435479+00:00", + "task": "CSOC-WP-0014-T01", + "status": "completed_bounded_observation", + "source_articles_sha256": "f33967c79c0f8a60a7d504f5d5e4082f8bc9e3c34f6a3160cb200fd380831d62", + "visited_articles": 5, + "route_matched_visits": 5, + "fresh_unchanged_parent_records": 5, + "typed_resources_observed": 0, + "empty_resource_envelopes": 0, + "response_status_counts": { + "200": 41 + }, + "response_timeouts": 0, + "response_read_failures": 2, + "retained_network_documents_checked": 294, + "retained_typed_resources_found": 0, + "linked_descriptor_targets_found": 0, + "settled_resource_controls_checked": 2, + "settled_control_label": "Add attachment", + "controls_activated": false, + "provider_queries_fabricated": false, + "resource_completeness_verified": false, + "resource_absence_inferred": false, + "operator_navigation_question_pending": true, + "original_capture_changed": false, + "production_imported": false, + "validation": { + "full_gate": "make check passed", + "junit": { + "tests": 596, + "errors": 0, + "failures": 0, + "skipped": 0 + }, + "synthetic_resource_node_tests": 4 + }, + "private_files": [ + { + "path": "capture.json", + "sha256": "81a8ee56ee6885e5d54ebd184bf309effb01fade3ff5d7ff6229e9d96f973751" + }, + { + "path": "index.html", + "sha256": "9218781bcf9273e2095494296d1d3f23d324e671c6d21920d69207b36181122f" + }, + { + "path": "ui-resource-controls-settled.json", + "sha256": "ad0b3823db1d241ce1473df83bd2a7bba32da49e44dc48e0e1d5954bd8a6faa9" + }, + { + "path": "ui-resource-controls.json", + "sha256": "4050f519bca5e4791ac0f0680a63eee2167c2b0e89c530ad045514bcb26a5da3" + }, + { + "path": "ui-resource-observation.json", + "sha256": "704363f4bb6ea5dcc606da13b667b979dd419d631b14b2c8517deb90723ccd1e" + } + ], + "implementation_files": [ + { + "path": "scripts/capture-bubble-resources.mjs", + "sha256": "647691c4283ee09eb8b320bbf3ef943eb8ad6be8d365e98a6b28dfd2a43bc92a" + }, + { + "path": "scripts/bubble-resource-utils.mjs", + "sha256": "e0cbd131bc28f67b3a88c45fb1e07cf12b33a71db051b5507e2e7ac14bf30170" + }, + { + "path": "scripts/bubble-corpus-utils.mjs", + "sha256": "aeb867c5a4fc332e3c994a24e47c7f93cf9d1b8f95738871ef9971f81571f502" + }, + { + "path": "scripts/tests/bubble-resource-utils.test.mjs", + "sha256": "d52dfc4fe8c3630ed020e9d6456e6836302c8f3e378bda424a1e3007873dee63" + }, + { + "path": "tests/test_bubble_capture_guards.py", + "sha256": "384e7da7eee76ca7f55cfcb45f9e2d8722509c9d0f3668288053b56f410047e1" + } + ], + "statehub_readback": { + "tasks": [ + { + "task_id": "0002882d-f156-5a5d-b524-d04793f4684d", + "status": "progress", + "resource_evidence_synced": true + }, + { + "task_id": "8d06cdee-e0ce-5c3e-bf98-705deed0ee62", + "status": "progress", + "resource_evidence_synced": true + } + ], + "t01_intervention_note_matches_source": true, + "fix_consistency_exit_code": 0, + "automation_errors": 0, + "assessment_failures": 0, + "warnings": 7 + } +} diff --git a/docs/release/bubble-resource-hover-2026-10-31.json b/docs/release/bubble-resource-hover-2026-10-31.json new file mode 100644 index 0000000..862cd32 --- /dev/null +++ b/docs/release/bubble-resource-hover-2026-10-31.json @@ -0,0 +1,14 @@ +{ + "schema": "coulomb.bubble-resource-hover-observation.v1", + "completed_at": "2026-10-31T18:35:54.302241+00:00", + "source_articles_sha256": "f33967c79c0f8a60a7d504f5d5e4082f8bc9e3c34f6a3160cb200fd380831d62", + "private_observation_sha256": "79d9fcae0a894a04ffbba73d91b1080dcfa745fd0d9a9eb9c89e63908d00a412", + "articles_visited": 5, + "all_source_routes_matched": true, + "visible_images_hovered": 42, + "typed_resources_found": 0, + "visual_overlay_download_points_to_already_retained_visual": true, + "ui_controls_activated": false, + "resource_completeness_verified": false, + "production_changes": false +} diff --git a/docs/release/bubble-scrap-disposition-2026-10-31.json b/docs/release/bubble-scrap-disposition-2026-10-31.json new file mode 100644 index 0000000..c4d4ddb --- /dev/null +++ b/docs/release/bubble-scrap-disposition-2026-10-31.json @@ -0,0 +1,22 @@ +{ + "schema": "coulomb.source-case-disposition.v1", + "status": "accepted_intentional_omission", + "source_field": "_list_custom_scrap", + "decision_id": "6db14274-4450-4ae2-b77a-cd4c88369eaa", + "decided_by": "Bernd", + "recorded_at": "2026-10-31T19:56:57.605591+00:00", + "user_instruction": "Lets ignore the scrap cases as they are legacy relicts that are no longer needed.", + "case_manifest_sha256": "c8c8394a7b801b7cf00ae3bb3411b0f12a52fd502b3f87ed0620124167d5a758", + "source_articles_sha256": "f33967c79c0f8a60a7d504f5d5e4082f8bc9e3c34f6a3160cb200fd380831d62", + "original_source_evidence_retained": true, + "page_bodies_or_dates_changed": false, + "production_data_deleted": false, + "other_source_exceptions_waived": false, + "case_numbers": [ + 1, + 2, + 3, + 4 + ], + "private_disposition_sha256": "ae9aef7b6520734a22b4c276ba161ef6206872009a41ddc3d044007fe65d045d" +} diff --git a/docs/release/bubble-scrap-navigation-2026-10-31.json b/docs/release/bubble-scrap-navigation-2026-10-31.json new file mode 100644 index 0000000..354e979 --- /dev/null +++ b/docs/release/bubble-scrap-navigation-2026-10-31.json @@ -0,0 +1,14 @@ +{ + "schema": "coulomb.scrap-navigation-observation.v1", + "completed_at": "2026-10-31T19:51:07.317085+00:00", + "source_articles_sha256": "f33967c79c0f8a60a7d504f5d5e4082f8bc9e3c34f6a3160cb200fd380831d62", + "navigators_opened": 4, + "typed_resource_records": 0, + "absence_inferred": false, + "source_mutations": false, + "production_imported": false, + "private_observation_sha256": "e2eaf4a94c311107c5fd1a43371ca9bfd7a2cf5f2010f40097441bf1f67712c8", + "private_case_manifest_sha256": "c8c8394a7b801b7cf00ae3bb3411b0f12a52fd502b3f87ed0620124167d5a758", + "private_review_path": "/home/worsch/.config/coulomb-social/auth/exports/scrap-review-20261031/index.html", + "remaining_required_evidence": "Original scrap viewing path/content or explicit per-case disposition" +} diff --git a/docs/release/bubble-visual-preparation-2026-10-31.json b/docs/release/bubble-visual-preparation-2026-10-31.json new file mode 100644 index 0000000..2b210f3 --- /dev/null +++ b/docs/release/bubble-visual-preparation-2026-10-31.json @@ -0,0 +1,110 @@ +{ + "schema": "coulomb.bubble-visual-preparation.v1", + "status": "captured_and_privately_prepared_not_imported", + "owner_task": "CSOC-WP-0014-T08", + "private_bundle": "~/.config/coulomb-social/auth/exports/visual-bundle-20261031", + "source_report_sha256": "75fcd87d17ea0c5042e903efd60e65839da7716e5399ff431a345486ddd10f01", + "capture": { + "references": 242, + "unique_urls": 108, + "downloaded_bytes": 168139156, + "materialized_pages": 236, + "body_rewriting_performed": false, + "grants_changed": false, + "browser_render_verified": false, + "production_imported": false + }, + "browser": { + "schema": "coulomb.private-visual-decode.v1", + "status": "all_captured_visuals_decoded", + "decoded_images": 90, + "bundle_sha256": "a255df2237d95588b21a5db059487ca0178d1df28c810ebaaf744c5e6491267a", + "browser_network_disabled": true, + "productive_fidelity_accepted": false + }, + "preservation": { + "schema": "coulomb.visual-preservation-proof.v1", + "verified_output_files": 738, + "verified_original_files": 545, + "frontmatter_only_changes": 303, + "all_source_bodies_and_dates_preserved": true, + "spaces_preserved": 71, + "original_blockers_preserved": true, + "unassigned_records_preserved": true, + "private_modes_verified": true, + "private_bundle_sha256": "a255df2237d95588b21a5db059487ca0178d1df28c810ebaaf744c5e6491267a", + "production_imported": false + }, + "boundaries": { + "cdn_origins": 1, + "https_only": true, + "redirects_allowed": false, + "cookies_or_authorization_sent": false, + "live_application_or_grants_changed": false, + "source_completeness_or_fidelity_accepted": false, + "all_other_links_attachments_reconciled": false + }, + "tests": { + "focused_passed": 24, + "full_gate_passed": 570, + "failures": 0, + "errors": 0, + "skipped": 0, + "warnings": 69, + "full_junit_sha256": "d11afa225571077d1d59c68711a5f0eb402de8ceff7f2f50c279fbba7f923349", + "final_verification_script": "native private bundle pass; final all-repo Ruff pass" + }, + "remaining": { + "source_graph_exception_groups": 4, + "unassigned_article_records": 1, + "body_link_references": 18, + "attachment_review_flags": 1, + "identity_and_explicit_grants": "unresolved", + "productive_fidelity_verdict": "pending" + }, + "sources": [ + { + "path": "scripts/capture-bubble-visuals.py", + "sha256": "c16c8cf5013a6f02c9a9f79f444aea82ced7d8642b0fb2d2dc5315fabe92be7c" + }, + { + "path": "scripts/verify-bubble-visuals.mjs", + "sha256": "2cb9625b0738028508957b8bc40a69b7ab2719046d20e0a69a0ac1a9394f56c5" + }, + { + "path": "scripts/verify-bubble-visual-preservation.py", + "sha256": "9fbc212bd668126a0f6ce6a458b1ca3fba1e14d4b44438c0697f63dc34b5bc3d" + }, + { + "path": "tests/test_bubble_visuals.py", + "sha256": "707fc9aa4ac0de5e40d33ee76571d74c141b361154a659f10c723a6597262006" + } + ], + "captured_at": "2026-10-31T13:48:44.558963+00:00", + "goal_task_projection_readback": { + "workplans": [ + { + "workplan": "CSOC-WP-0012", + "tasks": 14, + "done": 6 + }, + { + "workplan": "CSOC-WP-0013", + "tasks": 5, + "done": 2 + }, + { + "workplan": "CSOC-WP-0014", + "tasks": 8, + "done": 3 + }, + { + "workplan": "CSOC-WP-0015", + "tasks": 6, + "done": 3 + } + ], + "all_task_statuses_match_source": true, + "progress_event_id": "bb485164-d8ff-49cb-8c88-6aebe4e15cf0" + } +} diff --git a/docs/release/client-data-export-review-2026-10-30.md b/docs/release/client-data-export-review-2026-10-30.md index 45b8f5c..ea360fd 100644 --- a/docs/release/client-data-export-review-2026-10-30.md +++ b/docs/release/client-data-export-review-2026-10-30.md @@ -96,3 +96,19 @@ session revocation, process-log disposition and primary/mirror/protected-kit bac expiry. The existing independent-kit no-expiry protection must not be silently overridden. No client offboarding or productive deletion is authorized by this implementation or its tests. + +## October31 deployed return and offboarding scope + +Revision19 has since deployed the reviewed exporter. Its packaged current-point +proof verifies85selected local files and four record sets, no selected external +bindings and unchanged source data; the temporary ZIP was removed afterward. +See `identity-export-release-client-export-2026-10-31.json`. The earlier source-only +and81file statements above retain their original dated scope. + +The subsequent native scope check finds a second space outside the accepted +export tenant, with an external binding under the deployment default tenant. +Zero external bindings in the selected tenant must not be promoted to complete +instance attribution. The retained content PVC, five isolated database restores, +sessions, root journals and protected backup holds also need explicit disposition. +T12's concrete review is `client-offboarding-scope-review-2026-10-31.md`; it does +not authorize teardown or deletion of this active service. diff --git a/docs/release/client-offboarding-scope-2026-10-31.json b/docs/release/client-offboarding-scope-2026-10-31.json new file mode 100644 index 0000000..6237a65 --- /dev/null +++ b/docs/release/client-offboarding-scope-2026-10-31.json @@ -0,0 +1,157 @@ +{ + "schema": "coulomb.client-offboarding-scope.v1", + "recorded_at": "2026-10-31T14:55:21.318848+00:00", + "task": "CSOC-WP-0012-T12", + "task_status": "progress", + "current_application_scope": { + "observed_at": "2026-10-31T14:51:29.215499+00:00", + "scope": "accepted tenant:platform only; canonical client binding unresolved", + "database_name": "coulomb_social_db", + "record_counts": { + "members.user": 1, + "members.member": 1, + "spaces.space": 1, + "spaces.spacemembership": 1 + }, + "selected_local_content_files": 85, + "external_binding_count": 0, + "all_spaces_count": 2, + "selected_spaces_count": 1, + "session_row_count": 43, + "export_ownership_checks_passed": true, + "read_only_repeatable_read_transaction": true, + "external_writer_fence": false, + "coherent_export_created": false, + "source_changed": false + }, + "unselected_scope": { + "unselected_spaces": 1, + "unselected_external_bindings": 1, + "unselected_tenant_count": 1, + "unselected_tenants_match_deployment_default": true, + "unselected_spaces_with_creator": 0, + "canonical_client_assignment_inferred": false + }, + "native_content_storage": { + "observed_at": "2026-10-31T14:49:42.586579+00:00", + "deployment_uid": "2dbe661a-d232-4477-bbd1-833813077fbe", + "persistent_claims": [ + { + "name": "coulomb-social-content", + "uid": "9888abeb-9d52-4c70-a23d-b5494780d9c3", + "phase": "Bound", + "storage_class": "local-path", + "capacity": { + "storage": "2Gi" + }, + "helm_resource_policy": "keep", + "pv_name": "pvc-9888abeb-9d52-4c70-a23d-b5494780d9c3", + "pv_uid": "ac1aa73c-08d9-4561-995b-41060468e506", + "pv_reclaim_policy": "Delete", + "claim_uid_matches": true + } + ], + "configuration_changed": false + }, + "retained_local_restores": { + "containers": 5, + "running": 5, + "backing_database_volumes": 5, + "all_networks_internal": true, + "all_networks_single_container": true, + "published_ports": 0, + "public_receipt_linked_containers": 4, + "additional_private_receipt_sha256": "cc1d1be94da22756b52a00cc743ecd72b5cf8e956a7fd91288174dbb4499902b", + "additional_public_binding": "docs/release/identity-export-release-client-export-2026-10-31.json", + "all_five_have_receipt_bindings": true, + "stopped_or_deleted": false + }, + "scope_limits": { + "canonical_client_assignment_resolved": false, + "unselected_external_content_resolved": false, + "fresh_coherent_export_created": false, + "client_delivery_or_acceptance": false, + "sessions_decoded_or_revoked": false, + "instance_or_data_deleted": false, + "backup_hold_released": false, + "provider_expiry_performed": false, + "disposable_offboarding_rehearsal_performed": false, + "offboarding_accepted": false + }, + "private_inventory_files": [ + { + "name": "accepted-point-link.json", + "sha256": "eb54af2fe49bf90a02680d68c8f69335e8a591042479faa0367dc9c925f6f097" + }, + { + "name": "current-application-scope.json", + "sha256": "e24c3112c52e8ffd80a146c4dec8bd00e187ca0de14661644dd58206ebc535a6" + }, + { + "name": "native-storage.json", + "sha256": "e8f4c32462ffb9869ceaf8991874d14858705ebe59978eb9fc2743daa0135efc" + }, + { + "name": "restore-scope-linked.json", + "sha256": "8f2af25c9c80c69319bd6b61cf96094fe4054ed27fc1c63ae586962987971111" + }, + { + "name": "restore-scope-summary.json", + "sha256": "6d24beb8739c34eaa6c34b9d5829b161ba5e863eea1376103598b7a2f04822a5" + }, + { + "name": "retained-restores.json", + "sha256": "5b24822d8025c83011a286f097ca7f88125e42979864660ec5613e5b83a1323d" + }, + { + "name": "unselected-scope.json", + "sha256": "33df8b171bfc31713118b216f1fb3b113c716d42560a00585eaee0fa91dcd56d" + } + ], + "evidence_sources": [ + { + "path": "docs/release/identity-export-release-client-export-2026-10-31.json", + "sha256": "d1b9d88b850a5aed19159bee867c4b0723a7a1df9e13c179d57a3de9e9cb67af" + }, + { + "path": "docs/release/provider-kit-recovery-post-date-2026-10-31.json", + "sha256": "b7191972f4e4f7d0b74ae7bbdeb93aabdb79e2baf27ccb49108f6f30f417182c" + }, + { + "path": "docs/release/generation-retention-post-date-2026-10-31.json", + "sha256": "10c78d8a69c02f28f9ad5f126c4daac89f6988516e3234ea67eaee6c0b5be6df" + }, + { + "path": "coulomb_social/apps/members/client_export.py", + "sha256": "223a9ee42af6f17829aa263abcae595626e292b887de85820f48fdfa4a6363fa" + }, + { + "path": "/home/worsch/railiance-apps/helm/coulomb-social-values.yaml", + "sha256": "63a45d1cfb395eff7cfdf21ed2100fb9562b7a2ab6b896afecbebfaabacae547" + }, + { + "path": "/home/worsch/railiance-apps/charts/coulomb-social/templates/content-pvc.yaml", + "sha256": "5c4ddcaee2b3dcdf17da061b1a7a2cb1e0398d2f56f4ef31790cfaaf648b0fee" + }, + { + "path": "/home/worsch/the-custodian/canon/standards/business-app-service-contract_v0.1.md", + "sha256": "83d2799d39f957d88aa1d71c3f04f7c2a0e168390249dc409ce149128d44e80c" + } + ], + "statehub_readback": { + "task_id": "54af74b0-3212-537e-a02c-d2d8a091caf9", + "status": "progress", + "scope_review_synced": true, + "fix_consistency_exit_code": 0, + "automation_errors": 0, + "assessment_failures": 0, + "warnings": 8 + }, + "validation": { + "native_scope_assertions_passed": true, + "private_modes_and_hashes_verified": true, + "source_hashes_verified": true, + "json_round_trip_checked": true, + "application_code_changed": false + } +} diff --git a/docs/release/client-offboarding-scope-review-2026-10-31.md b/docs/release/client-offboarding-scope-review-2026-10-31.md new file mode 100644 index 0000000..09d6af9 --- /dev/null +++ b/docs/release/client-offboarding-scope-review-2026-10-31.md @@ -0,0 +1,81 @@ +# Client offboarding scope review — 2026-10-31 + +CSOC-WP-0012-T12 now has a concrete, non-destructive scope review. It remains +**progress**, not accepted or executable. This review neither closes the active +service nor authorizes deletion, session revocation, expiry or credential changes. + +The governing [service contract §6.2](/home/worsch/the-custodian/canon/standards/business-app-service-contract_v0.1.md) +requires support for complete client export and deletion: export, instance +teardown and backup expiry. Demonstrating that capability does not require +tearing down this active client now. T12 still needs its agreed scope, private +export delivery/acceptance and independently verified teardown/expiry procedure. + +## Verified scope and unresolved attribution + +The packaged revision19 export proof covers the accepted `tenant:platform`: +one User, Member, Space and SpaceMembership and85local files. Its coherent +07:13recovery point had no external bindings in that selected tenant. The temporary +export was removed after validation; this is not a delivered client archive. + +A fresh read-only application transaction at14:51Z repeats the ownership checks +and finds the same four record counts and85selected local files, with no selected +external binding. It also finds **two spaces in the instance**, one outside the +selected tenant. A separate bounded read confirms that unselected space has an +external binding and belongs to the configured default tenant (`tenant:coulomb`), +with no creator link. These facts do not establish its legal client, whether it +is dispensable, or whether its remote content has been preserved. T11's canonical +client/app/instance mapping and the disposition of this unselected binding must +precede any proposal to tear down the whole database or instance. + +The live read observed43session rows. Those rows were counted, not decoded or +attributed to the selected client. The read used a read-only transaction; app +writers were not fenced and no new coherent export was created. Do not combine +its counts with the older export hash to claim a newly verified deliverable. + +## Stores and owner returns + +| Store or boundary | Established evidence | Required offboarding return | +| --- | --- | --- | +| App SQL data, local principals, grants and admin history | Database `coulomb_social_db`; schema-derived inventory and selected-tenant export guards; one unselected externally bound space | Resolve client/tenant scope, produce and privately deliver a final coherent export, obtain acceptance, then review the database/role action through the deployment/database owners. Preserve the shared apps-pg cluster and other databases. | +| Local pages, assets, aliases and all revisions |85selected local files; deployed content PVC `coulomb-social-content`, UID9888abeb-9d52-4c70-a23d-b5494780d9c3, Bound2Gi/local-path | Export all assigned content, including deleted-page revisions. A future teardown must separately identify the claim/PV and verify its storage outcome. The PVC has `helm.sh/resource-policy: keep`; uninstalling the release alone is not content deletion. | +| App sessions and transient OIDC state |43current session rows; intentionally excluded from the client ZIP | Establish the approved app/tenant scope and revoke its sessions through a reviewed action. A count does not attribute anonymous/transient sessions; do not infer authority to revoke shared identity-provider sessions. | +| External content and identity links | One unselected externally bound space; OIDC/NetKingdom and user-engine are shared owners | Resolve the remote content and client assignment. Review only application-specific links/grants with their owners; do not delete shared human identities, provider clients or services based on local Member removal. | +| Node backup and release state | Existing paired archives, release capsules and date-operation journals; date journals can retain original index bytes outside CONTENT_ROOT | Include all assigned private bytes and journal/preimage copies in the exact retained/deleted set. T04's custody repair changes ownership/modes only and is not a disposition or expiry approval. | +| Local retained restores | Five running PostgreSQL recovery containers, five volume-backed stores; internal networks, one container each, no published ports | Preserve them pending disposition. The private inventory records container IDs, volume/mount identities, networks and links to existing restore receipts. Future cleanup must bind exact identities and verify both database volumes and corresponding plaintext content directories; stopping a container is not deletion. | +| Private source captures, review bundles and local archives | Source inventory spans71spaces and several source principals; private review/export copies are outside the live app | Classify ownership before deleting any directory. The whole source capture is not automatically the selected tenant's dataset. Preserve originals, accepted evidence and unresolved source records until explicit disposition. | +| Primary provider copies | Dedicated Coulomb application-archive prefix in shared `railiance-platform-pg-backup` | Provider owner must inventory exact versions, retain the final accepted export/recovery point as agreed, then execute admitted expiry. Do not delete the shared bucket or other applications' prefixes. | +| Mirrored generations and public receipts | Existing dated generation selection and exact-version readback; expiry remains separately admitted | Recompute the current exact keep/expiry set at execution. Rolling retention is not full client offboarding; deleting one mirror does not remove primary or protected copies. | +| Protected kits, proofs and separate recovery-key custody | Latest provider kit4166eb8b/version1793432303941465 is explicitly outside automatic expiry | Obtain explicit owner disposition for all client-bearing kit versions, proofs and associated recovery custody. Existing no-expiry protection remains in force; destroying a key alone is not a demonstrated complete offboarding procedure. | +| Logs, telemetry and notification ledgers | App/process logs and operator state are outside the ZIP; email-connect is a shared owner | Establish attribution and the permitted retention/expiry outcome with each owner. No log, mail-ledger or telemetry payload inventory or removal is claimed here. Preserve unrelated applications and required retained records. | +| Deployment, endpoint and credential projections | Revision19 and its declared resource/identity boundaries remain active | After accepted export and explicit teardown admission, remove only the bound instance resources/projections and verify routing, workloads, data stores and owner returns. Keep shared cluster, forge, identity, mail, telemetry and provider services. | + +The PVC's native PV reclaim policy is `Delete`, and its claim UID matches the +bound PVC. That policy is a configuration fact, not proof of deletion: the claim +has not been removed, and no storage teardown has been exercised. + +## Evidence and closure sequence + +The public aggregate proof is `client-offboarding-scope-2026-10-31.json`. Exact +local restore volume/path metadata stays in the private +`~/.config/coulomb-social/auth/exports/offboarding-review-20261031/` inventory. +Four restore containers link directly to public recovery receipts; the fifth +links through the private receipt whose SHA matches the published revision19 +client-export proof. All five remain running and untouched. + +1. Bind the canonical client to the actual tenant/instance scope and resolve the + unselected external binding; do not substitute the deployment default tenant. +2. Agree the complete store list and retention/expiry outcomes, including the + protected-kit hold, shared-owner stores and retained private copies. +3. Prepare the exact teardown/expiry procedure and demonstrate it on a disposable + scoped instance and provider-owner test resources. Keep production identities, + data and held backups out of that destructive rehearsal. +4. Produce a fresh coherent export for private delivery and obtain the required + client acceptance. The existing rehearsal archive no longer exists. +5. If actual offboarding is later requested, bind the then-current identities, + object versions, writer fence, accepted export and approvals before execution; + retain independent completion/exception receipts for every included store. + +This review does not choose retention durations, release holds, infer a client ID +or claim that steps3–5 have been performed. It supplies the missing concrete +scope for T12 while preserving T04 retention, T11 attribution, WP-0013 portability +and WP-0014 source/grant obligations. diff --git a/docs/release/content-acceptance-authorization-2026-10-31.json b/docs/release/content-acceptance-authorization-2026-10-31.json new file mode 100644 index 0000000..a976583 --- /dev/null +++ b/docs/release/content-acceptance-authorization-2026-10-31.json @@ -0,0 +1,12 @@ +{ + "schema": "coulomb.content-release-supervisor-disposition.v1", + "proposal_digest": "sha256:e96752ecfa7adef2fd1d9da48902c8e63de124ccfd272fe20ba79b758b3ca4a3", + "decision_id": "d9b70cc5-e035-40ae-8d0d-020190581202", + "approved_by": "Bernd", + "recorded_at": "2026-10-31T20:16:08.885956+00:00", + "user_instruction": "Good, then lets do deployment the productive acceptance work now.", + "status": "deployment_scope_approved_awaiting_founder_execution", + "execution_performed": false, + "human_only_execution_lane_changed": false, + "productive_acceptance_inferred": false +} diff --git a/docs/release/content-acceptance-build-2026-10-31.json b/docs/release/content-acceptance-build-2026-10-31.json new file mode 100644 index 0000000..4a60dbe --- /dev/null +++ b/docs/release/content-acceptance-build-2026-10-31.json @@ -0,0 +1,45 @@ +{ + "schema": "coulomb.content-acceptance-build.v1", + "completed_at": "2026-10-31T18:38:08.525103+00:00", + "source": "1139a6d30eee4f75317b46bd45120a15d818455f", + "image": "forgejo.coulomb.social/coulomb/coulomb-social@sha256:7906f461bc8d2d5204fd4fde67a251850f725b914452a66172919d6853672ed5", + "build_context": "clean restricted git archive", + "source_full_gate_tests": 568, + "runtime_packages_and_python_unchanged": true, + "runtime": { + "python": "3.12.16", + "distributions": { + "h11": "0.16.0", + "httpcore": "1.0.9", + "psycopg": "3.3.4", + "joserfc": "1.7.4", + "anyio": "4.14.2", + "cryptography": "50.0.0", + "idna": "3.18", + "certifi": "2026.7.22", + "packaging": "26.3", + "python-decouple": "3.8", + "psycopg-binary": "3.3.4", + "sqlparse": "0.5.5", + "whitenoise": "6.12.0", + "cffi": "2.1.1", + "markdown": "3.10.3", + "webencodings": "0.5.1", + "pycparser": "3.0", + "bleach": "6.4.0", + "authlib": "1.7.2", + "django": "6.1", + "typing_extensions": "4.16.0", + "gunicorn": "26.0.0", + "asgiref": "3.12.1", + "httpx": "0.28.1", + "dj-database-url": "3.1.2" + } + }, + "django_checks": { + "check": "System check identified no issues (0 silenced).", + "makemigrations": "No changes detected" + }, + "new_database_migrations": false, + "production_deployed": false +} diff --git a/docs/release/content-acceptance-fresh-client-export-2026-10-31.json b/docs/release/content-acceptance-fresh-client-export-2026-10-31.json new file mode 100644 index 0000000..f628b13 --- /dev/null +++ b/docs/release/content-acceptance-fresh-client-export-2026-10-31.json @@ -0,0 +1,67 @@ +{ + "schema": "coulomb.packaged-content-acceptance-recovery.v1", + "status": "packaged_recovered_export_and_views_verified", + "source": "1139a6d30eee4f75317b46bd45120a15d818455f", + "image": "forgejo.coulomb.social/coulomb/coulomb-social@sha256:7906f461bc8d2d5204fd4fde67a251850f725b914452a66172919d6853672ed5", + "restore_receipt_sha256": "9dacbd44544f93abf99354158cf610b4fb1f4e59611b22056b3086e0033231ab", + "retained_point_captured_at": "2026-10-31T20:21:01Z", + "candidate_application": { + "business_tables": 12, + "migration_rows_sha256": "43471516387c0e2a3d35971033ed6af330e2963efe4da93293d97633825d04af", + "business_rows": 93, + "business_rows_sha256": "7953b37b02b80d89ca6869dab6b2b0b996541de20ca0dd1b294a5f25572fb2ca", + "content_files": 86, + "content_files_sha256": "ed535246f4e8f46fcd7ce01f936127996aff873c6936b297f7272194b594aeda", + "global_slug_constraint": true, + "tenant_slug_constraint": true, + "migration_0003_applied": true, + "duplicate_slug_groups": 0, + "actual_owner_pages_rendered": 40, + "unsaved_outsider_page_denials": 40, + "production_identity_or_grant_created": false + }, + "previous_image_application": { + "business_tables": 12, + "migration_rows_sha256": "43471516387c0e2a3d35971033ed6af330e2963efe4da93293d97633825d04af", + "business_rows": 93, + "business_rows_sha256": "7953b37b02b80d89ca6869dab6b2b0b996541de20ca0dd1b294a5f25572fb2ca", + "content_files": 86, + "content_files_sha256": "ed535246f4e8f46fcd7ce01f936127996aff873c6936b297f7272194b594aeda", + "global_slug_constraint": true, + "tenant_slug_constraint": true, + "migration_0003_applied": true, + "duplicate_slug_groups": 0, + "actual_owner_pages_rendered": 40, + "unsaved_outsider_page_denials": 40, + "production_identity_or_grant_created": false + }, + "client_export": { + "status": "recovered_client_export_verified", + "schema": "coulomb.client-dataset-export.v1", + "content_files": 85, + "records": { + "members.user": 1, + "members.member": 1, + "spaces.space": 1, + "spaces.spacemembership": 1 + }, + "admin_events": 0, + "archive_sha256": "597f5929cae723e9f5d895afac6f5c96022049cb6bd938d4908d6aa56221f4ea", + "offboarding_complete": false, + "all_source_tables_compared": 13, + "all_source_files_compared": 86, + "archive_member_hashes_verified": true, + "selected_client_content_complete_locally": true, + "source_sql_and_content_unchanged": true, + "external_bindings_require_review": 0, + "sessions_and_credentials_excluded": true + }, + "source_content_files_unchanged": 86, + "all_tables_unchanged": true, + "application_source_overlay": false, + "network_internal": true, + "published_ports": 0, + "production_access": false, + "production_restore": false, + "driver_sha256": "ce8c34a0ad00f9c4ac1ba6dd2043884aa07c90e216074cae070626aa04bf1639" +} diff --git a/docs/release/content-acceptance-fresh-recovery-2026-10-31.json b/docs/release/content-acceptance-fresh-recovery-2026-10-31.json new file mode 100644 index 0000000..75706fb --- /dev/null +++ b/docs/release/content-acceptance-fresh-recovery-2026-10-31.json @@ -0,0 +1,37 @@ +{ + "schema": "coulomb.image-release-independent-recovery.v1", + "status": "fresh_point_independently_retrieved_and_restored", + "completed_at": "2026-10-31T20:27:35.058428+00:00", + "captured_at": "2026-10-31T20:21:01Z", + "primary_version": "1793478121590106", + "mirror_version": "1793478426025763", + "mirror_receipt_version": "1793478427159655", + "ciphertext_sha256": "91b4ffe4874a53e3422d604b7c3fa85f6551a481411ef82b2209993552486bfb", + "independent_reader_used": true, + "unpack": { + "status": "decrypted_pair_verified", + "version_id": "1793478121590106", + "encrypted_sha256": "91b4ffe4874a53e3422d604b7c3fa85f6551a481411ef82b2209993552486bfb", + "captured_at": "2026-10-31T20:21:01Z", + "database_sha256": "c5faa20ac0c443635393e04cf5b154710780d829d6c085edc098e2dd7525fc6a", + "content_sha256": "1bf31bafc19d8c4847f3793463189f13ce4b470ed96ccdc614b269acca6c12c0", + "application_restore_verified": false + }, + "restore": { + "status": "restored_verified", + "database_tables": 13, + "database_rows": 115, + "database_rows_and_sequences_match": true, + "content_files": 86, + "content_hashes_match": true, + "network_internal": true, + "published_ports": 0, + "postgres_image_id": "sha256:ff23cdce56cac62ada6f66013e1a50864c0abbe79d132d40b6e05bd80f378a70", + "elapsed_seconds": 4.42, + "content_was_empty": false + }, + "elapsed_seconds": 5.517, + "production_restore": false, + "current_kit_replaced": false, + "driver_sha256": "a88a63c0e990cc4d8d9180d1907ec367f2e7f54c81d00e79f38ded4f30384341" +} diff --git a/docs/release/content-acceptance-independent-postflight-2026-10-31.json b/docs/release/content-acceptance-independent-postflight-2026-10-31.json new file mode 100644 index 0000000..cdbc019 --- /dev/null +++ b/docs/release/content-acceptance-independent-postflight-2026-10-31.json @@ -0,0 +1,41 @@ +{ + "schema": "coulomb.content-acceptance-independent-postflight.v1", + "status": "deployed_image_and_complete_native_state_verified", + "proposal_digest": "sha256:e96752ecfa7adef2fd1d9da48902c8e63de124ccfd272fe20ba79b758b3ca4a3", + "receipt_sha256": "074ec03d62d43f35bd6c276213a28de4a7b0161132379221bec0e402b5334233", + "helm_revision": 20, + "image": "forgejo.coulomb.social/coulomb/coulomb-social@sha256:7906f461bc8d2d5204fd4fde67a251850f725b914452a66172919d6853672ed5", + "source": "1139a6d30eee4f75317b46bd45120a15d818455f", + "packaged_source_files_verified": 79, + "sole_ready_content_writer": true, + "all_frozen_capsule_files_verified": 23, + "installed_helper_hashes_verified": true, + "snapshot_pin_refreshed": true, + "public_health_landing_static_verified": true, + "postflight": { + "business_tables": 12, + "migration_rows_sha256": "43471516387c0e2a3d35971033ed6af330e2963efe4da93293d97633825d04af", + "business_rows": 93, + "business_rows_sha256": "7953b37b02b80d89ca6869dab6b2b0b996541de20ca0dd1b294a5f25572fb2ca", + "content_files": 86, + "content_files_sha256": "ed535246f4e8f46fcd7ce01f936127996aff873c6936b297f7272194b594aeda", + "global_slug_constraint": true, + "tenant_slug_constraint": true, + "migration_0003_applied": true, + "duplicate_slug_groups": 0, + "actual_owner_pages_rendered": 40, + "unsaved_outsider_page_denials": 40, + "production_identity_or_grant_created": false + }, + "fresh_paired_point": { + "captured_at": "2026-10-31T20:21:01Z", + "destination": "s3://railiance-platform-pg-backup/platform-pg/application-archives/coulomb-social/2026/10/31/20261031T202101Z-2ad4faf215939d67e82f67a4.tar.age", + "version_id": "1793478121590106", + "encrypted_sha256": "91b4ffe4874a53e3422d604b7c3fa85f6551a481411ef82b2209993552486bfb", + "encrypted_bytes": 2181848, + "image": "forgejo.coulomb.social/coulomb/coulomb-social@sha256:522bcd7fce92436765a60c1e4afa2598f3cc40913352153ff1c4319a99ca92ed" + }, + "timers_activated": false, + "production_data_restored": false, + "productive_acceptance_inferred": false +} diff --git a/docs/release/content-acceptance-preflight-2026-10-31.json b/docs/release/content-acceptance-preflight-2026-10-31.json new file mode 100644 index 0000000..6ef26b3 --- /dev/null +++ b/docs/release/content-acceptance-preflight-2026-10-31.json @@ -0,0 +1,25 @@ +{ + "schema": "coulomb.guarded-image-release.v1", + "proposal_digest": "sha256:e96752ecfa7adef2fd1d9da48902c8e63de124ccfd272fe20ba79b758b3ca4a3", + "started_at": "2026-10-31T18:41:21.634253+00:00", + "status": "live_release_preflight_verified", + "production_data_restored": false, + "timers_activated": false, + "node_request_percent": { + "cpu": 68, + "memory": 76 + }, + "preflight": { + "business_tables": 12, + "migration_rows_sha256": "43471516387c0e2a3d35971033ed6af330e2963efe4da93293d97633825d04af", + "business_rows": 93, + "business_rows_sha256": "7953b37b02b80d89ca6869dab6b2b0b996541de20ca0dd1b294a5f25572fb2ca", + "content_files": 86, + "content_files_sha256": "ed535246f4e8f46fcd7ce01f936127996aff873c6936b297f7272194b594aeda", + "global_slug_constraint": true, + "tenant_slug_constraint": true, + "migration_0003_applied": true, + "duplicate_slug_groups": 0 + }, + "live_changes": false +} \ No newline at end of file diff --git a/docs/release/content-acceptance-preflight-refresh-2026-10-31.json b/docs/release/content-acceptance-preflight-refresh-2026-10-31.json new file mode 100644 index 0000000..9d2c07b --- /dev/null +++ b/docs/release/content-acceptance-preflight-refresh-2026-10-31.json @@ -0,0 +1,25 @@ +{ + "schema": "coulomb.guarded-image-release.v1", + "proposal_digest": "sha256:e96752ecfa7adef2fd1d9da48902c8e63de124ccfd272fe20ba79b758b3ca4a3", + "started_at": "2026-10-31T20:14:42.622703+00:00", + "status": "live_release_preflight_verified", + "production_data_restored": false, + "timers_activated": false, + "node_request_percent": { + "cpu": 68, + "memory": 76 + }, + "preflight": { + "business_tables": 12, + "migration_rows_sha256": "43471516387c0e2a3d35971033ed6af330e2963efe4da93293d97633825d04af", + "business_rows": 93, + "business_rows_sha256": "7953b37b02b80d89ca6869dab6b2b0b996541de20ca0dd1b294a5f25572fb2ca", + "content_files": 86, + "content_files_sha256": "ed535246f4e8f46fcd7ce01f936127996aff873c6936b297f7272194b594aeda", + "global_slug_constraint": true, + "tenant_slug_constraint": true, + "migration_0003_applied": true, + "duplicate_slug_groups": 0 + }, + "live_changes": false +} \ No newline at end of file diff --git a/docs/release/content-acceptance-provenance-2026-10-31.json b/docs/release/content-acceptance-provenance-2026-10-31.json new file mode 100644 index 0000000..6c1e4c2 --- /dev/null +++ b/docs/release/content-acceptance-provenance-2026-10-31.json @@ -0,0 +1,11 @@ +{ + "schema": "coulomb.packaged-source-provenance.v1", + "image": "forgejo.coulomb.social/coulomb/coulomb-social@sha256:7906f461bc8d2d5204fd4fde67a251850f725b914452a66172919d6853672ed5", + "source": "1139a6d30eee4f75317b46bd45120a15d818455f", + "packaged_file_count": 79, + "all_packaged_source_bytes_match": true, + "source_revision_label_verified": true, + "registry_digest_resolves": true, + "application_source_overlay": false, + "manifest_sha256": "2eca0618df5bdf35928869c3b848be36190065a4cca8f855bba372df6c516556" +} diff --git a/docs/release/content-acceptance-receipt-2026-10-31.json b/docs/release/content-acceptance-receipt-2026-10-31.json new file mode 100644 index 0000000..bb4a840 --- /dev/null +++ b/docs/release/content-acceptance-receipt-2026-10-31.json @@ -0,0 +1,70 @@ +{ + "completed_at": "2026-10-31T20:22:30.428195+00:00", + "elapsed_seconds": 97.22, + "existing_global_constraint_preserved": true, + "fresh_paired_point": { + "age_recipient": "age1qm30jk8q0cezey652dk2dhcr3d9vs528v6lg8mavl6d4q3e3vgnqmrmyy3", + "application": "coulomb-social", + "application_restore_verified": false, + "captured_at": "2026-10-31T20:21:01Z", + "ciphertext_bytes": 2181848, + "ciphertext_sha256": "91b4ffe4874a53e3422d604b7c3fa85f6551a481411ef82b2209993552486bfb", + "completed_at": "2026-10-31T20:22:02.185627+00:00", + "content_sha256": "1bf31bafc19d8c4847f3793463189f13ce4b470ed96ccdc614b269acca6c12c0", + "database_sha256": "c5faa20ac0c443635393e04cf5b154710780d829d6c085edc098e2dd7525fc6a", + "destination": "s3://railiance-platform-pg-backup/platform-pg/application-archives/coulomb-social/2026/10/31/20261031T202101Z-2ad4faf215939d67e82f67a4.tar.age", + "encrypted_bytes": 2181848, + "encrypted_sha256": "91b4ffe4874a53e3422d604b7c3fa85f6551a481411ef82b2209993552486bfb", + "image": "forgejo.coulomb.social/coulomb/coulomb-social@sha256:522bcd7fce92436765a60c1e4afa2598f3cc40913352153ff1c4319a99ca92ed", + "immutable_create": true, + "local_archive": "20261031T202101Z-2ad4faf215939d67e82f67a4.tar.age", + "object_key": "platform-pg/application-archives/coulomb-social/2026/10/31/20261031T202101Z-2ad4faf215939d67e82f67a4.tar.age", + "pause_and_capture_seconds": 59.82, + "remote_retrieval_verified": true, + "schema": "platform.application-primary-archive.v1", + "status": "offsite_fetched_verified", + "version_id": "1793478121590106" + }, + "helm_revision": 20, + "node_request_percent": { + "cpu": 68, + "memory": 76 + }, + "pid": 1461378, + "postflight": { + "actual_owner_pages_rendered": 40, + "business_rows": 93, + "business_rows_sha256": "7953b37b02b80d89ca6869dab6b2b0b996541de20ca0dd1b294a5f25572fb2ca", + "business_tables": 12, + "content_files": 86, + "content_files_sha256": "ed535246f4e8f46fcd7ce01f936127996aff873c6936b297f7272194b594aeda", + "duplicate_slug_groups": 0, + "global_slug_constraint": true, + "migration_0003_applied": true, + "migration_rows_sha256": "43471516387c0e2a3d35971033ed6af330e2963efe4da93293d97633825d04af", + "production_identity_or_grant_created": false, + "tenant_slug_constraint": true, + "unsaved_outsider_page_denials": 40 + }, + "preflight": { + "business_rows": 93, + "business_rows_sha256": "7953b37b02b80d89ca6869dab6b2b0b996541de20ca0dd1b294a5f25572fb2ca", + "business_tables": 12, + "content_files": 86, + "content_files_sha256": "ed535246f4e8f46fcd7ce01f936127996aff873c6936b297f7272194b594aeda", + "duplicate_slug_groups": 0, + "global_slug_constraint": true, + "migration_0003_applied": true, + "migration_rows_sha256": "43471516387c0e2a3d35971033ed6af330e2963efe4da93293d97633825d04af", + "tenant_slug_constraint": true + }, + "private_values_printed": false, + "production_data_restored": false, + "proposal_digest": "sha256:e96752ecfa7adef2fd1d9da48902c8e63de124ccfd272fe20ba79b758b3ca4a3", + "public_health_landing_static_verified": true, + "schema": "coulomb.guarded-image-release.v1", + "snapshot_pin_refreshed": true, + "started_at": "2026-10-31T20:20:53.065197+00:00", + "status": "image_candidate_deployed_verified", + "timers_activated": false +} \ No newline at end of file diff --git a/docs/release/content-acceptance-reconciliation-2026-10-31.json b/docs/release/content-acceptance-reconciliation-2026-10-31.json new file mode 100644 index 0000000..50df0e0 --- /dev/null +++ b/docs/release/content-acceptance-reconciliation-2026-10-31.json @@ -0,0 +1,17 @@ +{ + "schema": "coulomb.content-release-reconciliation.v1", + "helm_revision": 20, + "snapshot_server_dry_run": { + "status": "reviewed", + "server_dry_run": true, + "image_pinned": true, + "pvc_read_only": true, + "db_scope": "coulomb_social_db", + "replicas_restored_to": 1 + }, + "backup_timers_inactive_disabled": true, + "snapshot_source_sha256": "23f92c96b840df61f92858004241e860e0ddb32ed6fd7448294d3c8278906e46", + "owner_live_values_match_sealed_values": true, + "recovery_container_stopped": true, + "snapshot_restore_regression_tests_passed": 9 +} diff --git a/docs/release/content-acceptance-recovery-2026-10-31.json b/docs/release/content-acceptance-recovery-2026-10-31.json new file mode 100644 index 0000000..ddc5c3b --- /dev/null +++ b/docs/release/content-acceptance-recovery-2026-10-31.json @@ -0,0 +1,67 @@ +{ + "schema": "coulomb.packaged-content-acceptance-recovery.v1", + "status": "packaged_recovered_export_and_views_verified", + "source": "1139a6d30eee4f75317b46bd45120a15d818455f", + "image": "forgejo.coulomb.social/coulomb/coulomb-social@sha256:7906f461bc8d2d5204fd4fde67a251850f725b914452a66172919d6853672ed5", + "restore_receipt_sha256": "3d6fe0b5d7426c9fb39ba22f18770319e0c89ba1981c61db171199d3b4e68524", + "retained_point_captured_at": "2026-10-31T07:13:52Z", + "candidate_application": { + "business_tables": 12, + "migration_rows_sha256": "43471516387c0e2a3d35971033ed6af330e2963efe4da93293d97633825d04af", + "business_rows": 92, + "business_rows_sha256": "6243955479f7861fcdcd79b62f7f1e9f9a27c05a17bd2c17b792ebc98b2fa7b6", + "content_files": 86, + "content_files_sha256": "ed535246f4e8f46fcd7ce01f936127996aff873c6936b297f7272194b594aeda", + "global_slug_constraint": true, + "tenant_slug_constraint": true, + "migration_0003_applied": true, + "duplicate_slug_groups": 0, + "actual_owner_pages_rendered": 40, + "unsaved_outsider_page_denials": 40, + "production_identity_or_grant_created": false + }, + "previous_image_application": { + "business_tables": 12, + "migration_rows_sha256": "43471516387c0e2a3d35971033ed6af330e2963efe4da93293d97633825d04af", + "business_rows": 92, + "business_rows_sha256": "6243955479f7861fcdcd79b62f7f1e9f9a27c05a17bd2c17b792ebc98b2fa7b6", + "content_files": 86, + "content_files_sha256": "ed535246f4e8f46fcd7ce01f936127996aff873c6936b297f7272194b594aeda", + "global_slug_constraint": true, + "tenant_slug_constraint": true, + "migration_0003_applied": true, + "duplicate_slug_groups": 0, + "actual_owner_pages_rendered": 40, + "unsaved_outsider_page_denials": 40, + "production_identity_or_grant_created": false + }, + "client_export": { + "status": "recovered_client_export_verified", + "schema": "coulomb.client-dataset-export.v1", + "content_files": 85, + "records": { + "members.user": 1, + "members.member": 1, + "spaces.space": 1, + "spaces.spacemembership": 1 + }, + "admin_events": 0, + "archive_sha256": "eb982edcdb94c976ab3d4e9797e4c34eb72a3ee3886a37e64508744ac172a8a8", + "offboarding_complete": false, + "all_source_tables_compared": 13, + "all_source_files_compared": 86, + "archive_member_hashes_verified": true, + "selected_client_content_complete_locally": true, + "source_sql_and_content_unchanged": true, + "external_bindings_require_review": 0, + "sessions_and_credentials_excluded": true + }, + "source_content_files_unchanged": 86, + "all_tables_unchanged": true, + "application_source_overlay": false, + "network_internal": true, + "published_ports": 0, + "production_access": false, + "production_restore": false, + "driver_sha256": "da85e5f7d5cf68f599a897a932d51399340bdd026a7cddf9603bef4aeb40909a" +} diff --git a/docs/release/content-acceptance-return-2026-10-31.sources.txt b/docs/release/content-acceptance-return-2026-10-31.sources.txt new file mode 100644 index 0000000..d1b13f7 --- /dev/null +++ b/docs/release/content-acceptance-return-2026-10-31.sources.txt @@ -0,0 +1,4 @@ +/home/worsch/.config/railiance-platform/coulomb-social-recovery/content-point-recovery-20261031/csoc-mirror-content-point.py: bc4d04c3f8ee3ca089fa9b038e6ab617eaababcb7f0aec94339e7f4eeb70f13a +/home/worsch/.config/railiance-platform/coulomb-social-recovery/content-point-recovery-20261031/csoc-recover-content-point.py: a88a63c0e990cc4d8d9180d1907ec367f2e7f54c81d00e79f38ded4f30384341 +/home/worsch/.config/railiance-platform/coulomb-social-recovery/content-point-recovery-20261031/csoc-verify-deployed-content.py: fb6079fb614a23eba0985c1d4a2e5b776e2d873f29c158cf6f38254d1415b387 +/home/worsch/.config/railiance-platform/coulomb-social-recovery/content-point-recovery-20261031/csoc-verify-fresh-content-point.py: ce8c34a0ad00f9c4ac1ba6dd2043884aa07c90e216074cae070626aa04bf1639 diff --git a/docs/release/content-acceptance-review-2026-10-31.md b/docs/release/content-acceptance-review-2026-10-31.md new file mode 100644 index 0000000..02667cd --- /dev/null +++ b/docs/release/content-acceptance-review-2026-10-31.md @@ -0,0 +1,137 @@ +# Editor attachment follow-up — deployed and independently verified + +Founder execution completed at Helm revision21 on2026-10-31T23:32:38Z. +All97business rows/98files remain unchanged;43owner/43outsider checks pass. +The command below is historical: do not rerun it. Evidence: +editor-attachments-{receipt,independent-postflight}-2026-11-01.json. + +Productive review found that revision20 preserves attachments but does not show +them in edit tabs. Source `31eaa581a65e454b6ef9979326c5bdfe10e13806` adds the +saved-file list/downloads to the editor, retains it after stale-edit refusal, +and opens management separately so unsaved text remains in its tab. +Main670, isolated568 and packaged110 checks pass, including the two-editor browser +journey. Candidate/previous images pass recovered views/export without changing +source data. Full native preflight passes against revision20/97rows/98files. +All prior93rows/86files are unchanged; review added2spaces/2memberships/12files. + +Image: `sha256:b58d858abd5b4acbd6b893c3f622f7a9fdb5e606164ab9866eb3cfaa04df1a0e`. +Proposal: `sha256:2cddcf38b7052287c10dce467a200a74cfa9699c666ae9ac538fc82917129e7a`. +This captures a fresh paired backup, performs the atomic image-only upgrade, +checks unchanged data and refreshes the backup image pin. Rollback preserves +writes and restores revision20's image/pin. No schema, timer, purge or mail change. + +Save work in open editor tabs before running; reopen the editor after verified +success. The current source-bound launcher explicitly requires founder-terminal +execution (tools/coulomb-image-release.py:1–5 in railiance-apps). + +```bash +python3 /home/worsch/railiance-apps/tools/coulomb-image-release.py \ + --plan /home/worsch/railiance-apps/release/coulomb-editor-attachments-31eaa58.json \ + --execute \ + --expect-digest sha256:2cddcf38b7052287c10dce467a200a74cfa9699c666ae9ac538fc82917129e7a +``` + +At the prompt: + +```text +DEPLOY sha256:2cddcf38b7052287c10dce467a200a74cfa9699c666ae9ac538fc82917129e7a +``` + +Return the JSON result for independent verification. The next productive review +must check both edit tabs, downloads and stale-edit text/attachment preservation. +Evidence: editor-attachments-{validation,provenance,tests,recovery,target,baseline-change,preflight}-2026-10-31.json. + +--- + +# Combined content release for WP-0013 + +Deployed and independently verified at Helm revision20, completed +2026-10-31T20:22:30Z. The command below is historical: do not rerun it. +All93business rows/86files are unchanged;40owner views/40outsider denials pass. +See content-acceptance-independent-postflight-2026-10-31.json. + +The candidate adds the requested attachment action menu, including author uploads +and removal, authenticated downloads, and independent attachments on copies and +moves. It also restores ordered-list starting numbers and fixes interrupted +copy/move asset recovery. Removal detaches the current association; earlier +revisions retain the file. Existing body links and visuals remain editable in the +page editor. The upload limit is16 MiB per file and100 associations per page. + +Source: `1139a6d30eee4f75317b46bd45120a15d818455f` on +`csoc-wp0013-content-release`. Immutable image: +`sha256:7906f461bc8d2d5204fd4fde67a251850f725b914452a66172919d6853672ed5`. +The working repository passes670tests; the isolated release branch passes568; +the actual image passes110focused checks without application-source overlays. +Every packaged source file matches that commit. Runtime dependencies/Python and +database migrations are unchanged. Historical release packets remain unchanged. + +Both the candidate and previous image render40owner pages and deny40outsiders on +the retained independently restored07:13point. The packaged client export verifies +85files while all13SQL tables/86source files remain unchanged. The temporary +recovery database was returned to its previously stopped state. This retained +point has92business rows; current production has93. Session/login-only drift was +previously reviewed and its fingerprints are unchanged. Execution creates and +reads back a fresh paired backup before Helm; independent recovery of that new +point is now verified: all13tables/115rows and86files restore exactly; the +deployed and previous packages pass40owner/40outsider checks, and client export +verifies85files without altering source data. See content-acceptance-fresh- +{recovery,client-export}-2026-10-31.json. + +The route and inactive installation prerequisites are independently verified. +Full recurring backup/service acceptance remains WP-0012. This proposal does not +activate timers, purge retained data, restore production data or send email. + +## Exact attended action + +Plan: `/home/worsch/railiance-apps/release/coulomb-content-acceptance-1139a6d.json`. +Proposal: `sha256:e96752ecfa7adef2fd1d9da48902c8e63de124ccfd272fe20ba79b758b3ca4a3`. +Baseline: revision19/image522bcd7f,93business rows/86files. The controller checks +complete data/spec fingerprints, creates the paired recovery point, performs the +atomic image-only Helm upgrade, verifies live views/data and refreshes the +snapshot image pin. Rollback restores the prior image/pin while preserving writes. +An older image retains the new attachment metadata/files but lacks the new menu. + +Full native preflight reports `live_release_preflight_verified` at this exact +digest (receipt: content-acceptance-preflight-2026-10-31.json). Resolved approval +decision: d9b70cc5-e035-40ae-8d0d-020190581202. Executed command: + +```bash +python3 /home/worsch/railiance-apps/tools/coulomb-image-release.py \ + --plan /home/worsch/railiance-apps/release/coulomb-content-acceptance-1139a6d.json \ + --execute \ + --expect-digest sha256:e96752ecfa7adef2fd1d9da48902c8e63de124ccfd272fe20ba79b758b3ca4a3 +``` + +At its prompt: + +```text +DEPLOY sha256:e96752ecfa7adef2fd1d9da48902c8e63de124ccfd272fe20ba79b758b3ca4a3 +``` + +Founder-terminal execution is required by the source-bound launcher and the +standing supervised execution contract in +`the-custodian/docs/agent-autonomy-decision.md`: “The supervisor approves that +proposal or performs it through the privileged execution path.” Bernd separately +approved and executed this proposal; the execution gate is satisfied. + +The action pauses writes during backup capture and the Recreate rollout. It adds +one encrypted primary backup object and temporary scratch within the admitted +capacity limits; provider cost is unpriced and no autopilot budget is claimed. +Check the durable receipt before any retry. Required final status: +`image_candidate_deployed_verified`. Receipt path: +`/var/lib/coulomb-social-backup/releases/e96752ecfa7adef2fd1d9da48902c8e63de124ccfd272fe20ba79b758b3ca4a3/receipt.json`. + +## Productive review after verified deployment + +Use the prepared UC-003–006 steps in +[productive-content-acceptance-session.md](../usecases/productive-content-acceptance-session.md). +On a disposable page, open Attachments, add a file, download it as an authorized +reader, duplicate/move the page, and remove the copied association. Verify that +the original remains independent and revoked/outsider access is denied. Record +attachment, authoring, copy/move, sharing and portability verdicts separately. + +The three custom-topic rendering cases are accepted for now under decision +69ec9380. The two downloadable attachments are recovered. Bernd explicitly omitted the +four reviewed scrap cases as obsolete legacy data under resolved decision6db14274. +Other source-body/graph exceptions remain with source reconciliation. +No productive acceptance or WP-0013 completion is inferred from these tests. diff --git a/docs/release/content-acceptance-target-2026-10-31.json b/docs/release/content-acceptance-target-2026-10-31.json new file mode 100644 index 0000000..cd50ca6 --- /dev/null +++ b/docs/release/content-acceptance-target-2026-10-31.json @@ -0,0 +1,21 @@ +{ + "baseline_revision": 19, + "baseline_helm_manifest_sha256": "93cb277abd1aea0657be482e458dcfb268fe0bfbd45d90aa1faedcc7f5bce499", + "deployment_uid": "2dbe661a-d232-4477-bbd1-833813077fbe", + "baseline_spec_sha256": "42e091e52d5dae3c1f986e4898c62057fa80846adfce049a75d8fbd4681180ea", + "candidate_spec_sha256": "8d79c64cff4bd2c62f03494e80847dc99b965ddabf35645da67e8d1020220798", + "old_image": "forgejo.coulomb.social/coulomb/coulomb-social@sha256:522bcd7fce92436765a60c1e4afa2598f3cc40913352153ff1c4319a99ca92ed", + "expected_data": { + "business_tables": 12, + "migration_rows_sha256": "43471516387c0e2a3d35971033ed6af330e2963efe4da93293d97633825d04af", + "business_rows": 93, + "business_rows_sha256": "7953b37b02b80d89ca6869dab6b2b0b996541de20ca0dd1b294a5f25572fb2ca", + "content_files": 86, + "content_files_sha256": "ed535246f4e8f46fcd7ce01f936127996aff873c6936b297f7272194b594aeda", + "global_slug_constraint": true, + "tenant_slug_constraint": true, + "migration_0003_applied": true, + "duplicate_slug_groups": 0 + }, + "image_only_server_preview": true +} diff --git a/docs/release/content-acceptance-tests-2026-10-31.json b/docs/release/content-acceptance-tests-2026-10-31.json new file mode 100644 index 0000000..971fb31 --- /dev/null +++ b/docs/release/content-acceptance-tests-2026-10-31.json @@ -0,0 +1,13 @@ +{ + "schema": "coulomb.packaged-content-acceptance-tests.v1", + "image": "forgejo.coulomb.social/coulomb/coulomb-social@sha256:7906f461bc8d2d5204fd4fde67a251850f725b914452a66172919d6853672ed5", + "source": "1139a6d30eee4f75317b46bd45120a15d818455f", + "tests": 110, + "failures": 0, + "errors": 0, + "skipped": 0, + "network": "none", + "read_only_root": true, + "application_source_overlay": false, + "synthetic_fixtures_only": true +} diff --git a/docs/release/content-lifecycle-baseline-tests-2026-10-31.json b/docs/release/content-lifecycle-baseline-tests-2026-10-31.json new file mode 100644 index 0000000..6b3e676 --- /dev/null +++ b/docs/release/content-lifecycle-baseline-tests-2026-10-31.json @@ -0,0 +1,25 @@ +{ + "schema": "coulomb.packaged-content-lifecycle-tests.v1", + "source": "23ffe2cf4b214666754a2420558943438e54251a", + "image_id": "sha256:522bcd7fce92436765a60c1e4afa2598f3cc40913352153ff1c4319a99ca92ed", + "tests": 29, + "failures": 4, + "expected_baseline_regressions": true, + "failed_nodes": [ + "test_process_death_during_copy_or_move_is_recoverable[copy_page-during_first_asset]", + "test_process_death_during_copy_or_move_is_recoverable[copy_page-during_second_asset]", + "test_process_death_during_copy_or_move_is_recoverable[transfer_page-during_first_asset]", + "test_process_death_during_copy_or_move_is_recoverable[transfer_page-during_second_asset]" + ], + "errors": 0, + "skipped": 0, + "network": "none", + "read_only_root": true, + "application_source_overlay": false, + "test_tooling_injected": true, + "application_modules_from_image_verified": true, + "production_access": false, + "junit_sha256": "edc52bd3a321d028d255b3d478020b3383d94747957ffe2985583b1f0741cb85", + "driver_sha256": "85b65114997c7e7c5323cb77a0043e495ec132506eef07dd394c6a6361380d36", + "test_source_sha256": "84a25a22a3cbda397e39c0508db8ea5aa90e6c467ccfa7da2b568d174d1ec449" +} diff --git a/docs/release/content-lifecycle-candidate-2026-10-31.sources.txt b/docs/release/content-lifecycle-candidate-2026-10-31.sources.txt new file mode 100644 index 0000000..12056be --- /dev/null +++ b/docs/release/content-lifecycle-candidate-2026-10-31.sources.txt @@ -0,0 +1,4 @@ +/home/worsch/coulomb-social/coulomb_social/apps/spaces/pageops.py: 055ef25b5efc75798cf17ffa68739de4d4099d2cab4d041e7c7eacdfaf43803e +/home/worsch/coulomb-social/tests/test_content_recovery.py: 84a25a22a3cbda397e39c0508db8ea5aa90e6c467ccfa7da2b568d174d1ec449 +/home/worsch/coulomb-social/scripts/verify-packaged-source.py: 99d734f20331e8d699081d3d77e7a0d0b3bc676be69ce101377bc69eae6c70d5 +/tmp/csoc-packaged-lifecycle-tests-20261031.py: 85b65114997c7e7c5323cb77a0043e495ec132506eef07dd394c6a6361380d36 diff --git a/docs/release/content-lifecycle-candidate-build-2026-10-31.json b/docs/release/content-lifecycle-candidate-build-2026-10-31.json new file mode 100644 index 0000000..07207bc --- /dev/null +++ b/docs/release/content-lifecycle-candidate-build-2026-10-31.json @@ -0,0 +1,119 @@ +{ + "schema": "coulomb.content-lifecycle-candidate-build.v1", + "completed_at": "2026-10-31T11:58:26.697591+00:00", + "source": "f306dbfa2604e4815523667740be2925c333b84e", + "tag": "forgejo.coulomb.social/coulomb/coulomb-social:f306dbfa2604", + "image": "forgejo.coulomb.social/coulomb/coulomb-social@sha256:d3f2f5bb90f0ba79f4e1d8098d2eb06f8940b8789e5ecb851914bbab364597ec", + "registry_digest_independently_verified": true, + "source_revision_label_verified": true, + "build_context": "Clean git archive restricted to Dockerfile, dockerignore, lock, project, manage.py, app and entrypoint.", + "build_inputs": [ + { + "path": "Dockerfile", + "sha256": "d0e197a18db8980eb3e51cca842d693e10ab0b4208627bba28691d10719d4a00" + }, + { + "path": ".dockerignore", + "sha256": "901c8b95d95549d0e78c1c3d6d0538bf0252c8a77e9409129e0cadfa55c7c56c" + }, + { + "path": "pyproject.toml", + "sha256": "5758c56fd1be89b973861ccc5ecbadb495e742ee13ebd7e19a221a0f119c54ee" + }, + { + "path": "uv.lock", + "sha256": "e8ebb6fc4b5abd1ac80f1c9012f1983642d0528a8a213cfcc394261606fad887" + }, + { + "path": "scripts/docker-entrypoint.sh", + "sha256": "70100256ca41ae6afe77078c810a8b6012472f0a741919f403f32088f98ab463" + } + ], + "runtime": { + "previous": { + "python": "3.12.16", + "distributions": { + "h11": "0.16.0", + "httpcore": "1.0.9", + "psycopg": "3.3.4", + "joserfc": "1.7.4", + "anyio": "4.14.2", + "cryptography": "50.0.0", + "idna": "3.18", + "certifi": "2026.7.22", + "packaging": "26.3", + "python-decouple": "3.8", + "psycopg-binary": "3.3.4", + "sqlparse": "0.5.5", + "whitenoise": "6.12.0", + "cffi": "2.1.1", + "markdown": "3.10.3", + "webencodings": "0.5.1", + "pycparser": "3.0", + "bleach": "6.4.0", + "authlib": "1.7.2", + "django": "6.1", + "typing_extensions": "4.16.0", + "gunicorn": "26.0.0", + "asgiref": "3.12.1", + "httpx": "0.28.1", + "dj-database-url": "3.1.2" + } + }, + "candidate": { + "python": "3.12.16", + "distributions": { + "h11": "0.16.0", + "httpcore": "1.0.9", + "psycopg": "3.3.4", + "joserfc": "1.7.4", + "anyio": "4.14.2", + "cryptography": "50.0.0", + "idna": "3.18", + "certifi": "2026.7.22", + "packaging": "26.3", + "python-decouple": "3.8", + "psycopg-binary": "3.3.4", + "sqlparse": "0.5.5", + "whitenoise": "6.12.0", + "cffi": "2.1.1", + "markdown": "3.10.3", + "webencodings": "0.5.1", + "pycparser": "3.0", + "bleach": "6.4.0", + "authlib": "1.7.2", + "django": "6.1", + "typing_extensions": "4.16.0", + "gunicorn": "26.0.0", + "asgiref": "3.12.1", + "httpx": "0.28.1", + "dj-database-url": "3.1.2" + } + }, + "runtime_packages_equal": true, + "python_versions_equal": true, + "django_check": { + "returncode": 0, + "output": "System check identified no issues (0 silenced)." + }, + "migration_drift": { + "returncode": 0, + "output": "No changes detected" + } + }, + "source_delta_from_revision_19": [ + "coulomb_social/apps/spaces/pageops.py" + ], + "new_database_migrations": false, + "production_deployed": false, + "live_release_pins_modified": false, + "source_full_gate_tests": 546, + "packaged_lifecycle_tests": 29, + "previous_image_expected_lifecycle_failures": 4, + "owner_release_task": "RAPPS-WP-0015-T07", + "remaining": [ + "Exact current-target release packet, paired rollback evidence and candidate/previous-image restored-store checks.", + "Attended image release and independent native postflight.", + "Productive lifecycle verdict; retention policy already accepted under c3a191f2-ce06-483f-82d7-ef8c54ea0d5e." + ] +} diff --git a/docs/release/content-lifecycle-candidate-provenance-2026-10-31.json b/docs/release/content-lifecycle-candidate-provenance-2026-10-31.json new file mode 100644 index 0000000..60aefca --- /dev/null +++ b/docs/release/content-lifecycle-candidate-provenance-2026-10-31.json @@ -0,0 +1,20 @@ +{ + "schema": "coulomb.packaged-source-provenance.v1", + "image": "forgejo.coulomb.social/coulomb/coulomb-social@sha256:d3f2f5bb90f0ba79f4e1d8098d2eb06f8940b8789e5ecb851914bbab364597ec", + "network": "none", + "read_only_root": true, + "entrypoint_bypassed": true, + "database_contact": false, + "production_changes": false, + "comparisons": [ + { + "commit": "f306dbfa2604e4815523667740be2925c333b84e", + "packaged_file_count": 80, + "source_file_count": 80, + "different_files": 0, + "all_packaged_source_bytes_match": true + } + ], + "actual_packaged_manifest_sha256": "b5c8a16b4b3d30cc9d8a7a9f3ed61bd8cb0e182589aceb5e4127fff27a8ae482", + "dependency_or_build_reproducibility_proved": false +} diff --git a/docs/release/content-lifecycle-candidate-recovery-2026-10-31.json b/docs/release/content-lifecycle-candidate-recovery-2026-10-31.json new file mode 100644 index 0000000..72b4365 --- /dev/null +++ b/docs/release/content-lifecycle-candidate-recovery-2026-10-31.json @@ -0,0 +1,65 @@ +{ + "schema": "coulomb.packaged-content-lifecycle-recovery.v1", + "status": "packaged_recovered_client_export_and_views_verified", + "source": "f306dbfa2604e4815523667740be2925c333b84e", + "image_id": "sha256:d3f2f5bb90f0ba79f4e1d8098d2eb06f8940b8789e5ecb851914bbab364597ec", + "restore_receipt_sha256": "966d6640e42e2f4940c66d1cd5c8a802cd950de0305572f3c8b720ee3d81ae2b", + "client_export": { + "status": "recovered_client_export_verified", + "schema": "coulomb.client-dataset-export.v1", + "content_files": 85, + "records": { + "members.user": 1, + "members.member": 1, + "spaces.space": 1, + "spaces.spacemembership": 1 + }, + "admin_events": 0, + "archive_sha256": "e364a61570e0083e729a3f314eb707c57347200438bd98170c77b8f7fcc2b337", + "offboarding_complete": false, + "all_source_tables_compared": 13, + "all_source_files_compared": 86, + "archive_member_hashes_verified": true, + "selected_client_content_complete_locally": true, + "source_sql_and_content_unchanged": true, + "external_bindings_require_review": 0, + "sessions_and_credentials_excluded": true + }, + "candidate_application": { + "status": "isolated_application_recovery_verified", + "recovered_spaces_with_content": 1, + "actual_pages_rendered_through_view": 40, + "restored_owner_access_verified": true, + "synthetic_viewer_reads": 1, + "synthetic_viewer_mutation_denials": 1, + "same_tenant_ungranted_page_denials": 40, + "fixture_transaction_rolled_back": true, + "production_access": false, + "external_identity_provider_used": false, + "real_browser_journey_verified": false + }, + "previous_image_application": { + "status": "isolated_application_recovery_verified", + "recovered_spaces_with_content": 1, + "actual_pages_rendered_through_view": 40, + "restored_owner_access_verified": true, + "synthetic_viewer_reads": 1, + "synthetic_viewer_mutation_denials": 1, + "same_tenant_ungranted_page_denials": 40, + "fixture_transaction_rolled_back": true, + "production_access": false, + "external_identity_provider_used": false, + "real_browser_journey_verified": false + }, + "source_content_files_unchanged": 86, + "all_tables_unchanged_after_both_view_checks": true, + "application_source_overlay": false, + "export_archive_removed": true, + "network_internal": true, + "published_ports": 0, + "production_access": false, + "production_restore": false, + "post_date_correction_data_claimed": true, + "retained_point_captured_at": "2026-10-31T07:13:52Z", + "driver_sha256": "92b66225e5f9bdf03d0a0999b613370c42341a2ceef468f1743fb4301884cb25" +} diff --git a/docs/release/content-lifecycle-candidate-tests-2026-10-31.json b/docs/release/content-lifecycle-candidate-tests-2026-10-31.json new file mode 100644 index 0000000..a0748e4 --- /dev/null +++ b/docs/release/content-lifecycle-candidate-tests-2026-10-31.json @@ -0,0 +1,20 @@ +{ + "schema": "coulomb.packaged-content-lifecycle-tests.v1", + "source": "f306dbfa2604e4815523667740be2925c333b84e", + "image_id": "sha256:d3f2f5bb90f0ba79f4e1d8098d2eb06f8940b8789e5ecb851914bbab364597ec", + "tests": 29, + "failures": 0, + "expected_baseline_regressions": false, + "failed_nodes": [], + "errors": 0, + "skipped": 0, + "network": "none", + "read_only_root": true, + "application_source_overlay": false, + "test_tooling_injected": true, + "application_modules_from_image_verified": true, + "production_access": false, + "junit_sha256": "f2968ef1048ec9af68a187b967d558db0f6ee76e765a8106fb7fab284953511f", + "driver_sha256": "85b65114997c7e7c5323cb77a0043e495ec132506eef07dd394c6a6361380d36", + "test_source_sha256": "84a25a22a3cbda397e39c0508db8ea5aa90e6c467ccfa7da2b568d174d1ec449" +} diff --git a/docs/release/content-lifecycle-release-2026-10-31.sources.txt b/docs/release/content-lifecycle-release-2026-10-31.sources.txt new file mode 100644 index 0000000..cdb9490 --- /dev/null +++ b/docs/release/content-lifecycle-release-2026-10-31.sources.txt @@ -0,0 +1,3 @@ +/tmp/csoc-content-release-observation-20261031.py: 1065625ee5b0736ae68a5230684f0effcb2c92cf0f814154ee86450b5a8fda4a +/tmp/csoc-content-row-drift-20261031.py: 49c8f0b8a92524205e2b190bbefec76d9cc38d966c4d20ffa7391e85ff17bf0e +/tmp/csoc-packaged-lifecycle-recovery-20261031.py: 92b66225e5f9bdf03d0a0999b613370c42341a2ceef468f1743fb4301884cb25 diff --git a/docs/release/content-lifecycle-release-preflight-2026-10-31.json b/docs/release/content-lifecycle-release-preflight-2026-10-31.json new file mode 100644 index 0000000..e62ada5 --- /dev/null +++ b/docs/release/content-lifecycle-release-preflight-2026-10-31.json @@ -0,0 +1,7 @@ +{ + "proposal_digest": "sha256:c9c3231620721ef3ea9b2bf22fc6c62fe3ed8efb91386cf7ec96acc6959dae08", + "execution_started": false, + "status": "release_refused_or_failed", + "failure_type": "Refused", + "failure_reason": "inactive_backup_installation_required_before_image_release" +} \ No newline at end of file diff --git a/docs/release/content-lifecycle-release-preparation-2026-10-31.json b/docs/release/content-lifecycle-release-preparation-2026-10-31.json new file mode 100644 index 0000000..fd558c3 --- /dev/null +++ b/docs/release/content-lifecycle-release-preparation-2026-10-31.json @@ -0,0 +1,79 @@ +{ + "schema": "coulomb.content-release-preparation.v1", + "prepared_at": "2026-10-31T12:16:52.085864+00:00", + "status": "prepared_waiting_for_inactive_backup_installation", + "proposal_digest": "sha256:c9c3231620721ef3ea9b2bf22fc6c62fe3ed8efb91386cf7ec96acc6959dae08", + "capsule_members": 20, + "owner_task": "RAPPS-WP-0015-T07", + "owner_task_id": "8f448934-bdfe-539f-8cab-a739ec752491", + "consumer_task": "CSOC-WP-0013-T05", + "baseline_revision": 19, + "current_business_rows": 93, + "candidate_image": "forgejo.coulomb.social/coulomb/coulomb-social@sha256:d3f2f5bb90f0ba79f4e1d8098d2eb06f8940b8789e5ecb851914bbab364597ec", + "prerequisite_installation_digest": "sha256:8f442bf0ebed8bbf3b7f227b52081df52613f3f5f6874c4a59511cfc2ba06d42", + "ordering": [ + "historical custody repair", + "recurring alert route", + "inactive backup-source installation", + "content image preflight and supervised release", + "native recurring proof and schedule activation" + ], + "controller_tests": { + "tests": 40, + "failures": 0, + "errors": 0, + "skipped": 0 + }, + "helm_lint": "passed", + "image_only_local_render_difference": true, + "candidate_image_only_server_preview_verified": true, + "native_full_preflight": "refused_expected_missing_installation", + "full_preflight_passed": false, + "historical_custody_packet_digest_unchanged": true, + "inactive_installer_packet_digest_unchanged": true, + "release_executed": false, + "fresh_backup_created_by_preparation": false, + "production_data_restored": false, + "timers_activated": false, + "new_resources_committed": 0, + "cost_risk_review": { + "additional_infrastructure_reservations": 0, + "new_backup_objects": 1, + "encrypted_file_limit_bytes": 1073741824, + "incremental_provider_cost_eur": null, + "autopilot_budget_claimed": false, + "risks": [ + "Brief application pause for paired capture and Recreate image rollout.", + "Rollback restores previous image/helper without rolling back productive data.", + "Uncertain or changed baseline refuses; inspect durable receipt before retry." + ] + }, + "files": [ + { + "repo": "railiance-apps", + "path": "tools/coulomb-image-release.py", + "sha256": "4cefb7b4ce7fb7eece1cd78ce3c1712a2b5cc2f9c81acfed06e3c6ac7c4abad3" + }, + { + "repo": "railiance-apps", + "path": "tools/coulomb_image_release_node.py", + "sha256": "5547b3809f34178e48404cd1590f665729b062b4333f561e2da81b56d435dece" + }, + { + "repo": "railiance-apps", + "path": "tests/test_coulomb_image_release.py", + "sha256": "dc9cd51bd0698b46c5e2a4f2f0fb8f89eceed722a92bd7b03f7278ed77a497d4" + }, + { + "repo": "railiance-apps", + "path": "release/coulomb-content-lifecycle-f306dbf.json", + "sha256": "1bcd840aee2a93130838b7430de279c56dd7aecbce4244765a85be9d0a7130e2" + } + ], + "native_no_attempt_readback": { + "release_journal_exists": false, + "release_receipt_exists": false + }, + "sequencing_decision_id": "de5a4528-0864-4769-b9d0-2f658b0310c6", + "owner_controller_commit": "9c748846413c6db3f13ae3e55627fe3f6ad89194" +} diff --git a/docs/release/content-lifecycle-release-proposal-2026-10-31.json b/docs/release/content-lifecycle-release-proposal-2026-10-31.json new file mode 100644 index 0000000..5f633e1 --- /dev/null +++ b/docs/release/content-lifecycle-release-proposal-2026-10-31.json @@ -0,0 +1,90 @@ +{ + "schema": "coulomb.reviewed-image-release.v1", + "proposal_id": "CSOC-CONTENT-LIFECYCLE-20261031", + "scope": "ADMINISTER @ realm:kubernetes/railiance01; supervised founder execution only", + "operation": "image_only_existing_migration_0003", + "cluster_uid": "a553c742-0115-43d4-99a4-a5ca56fe0786", + "baseline_revision": 19, + "baseline_helm_manifest_sha256": "93cb277abd1aea0657be482e458dcfb268fe0bfbd45d90aa1faedcc7f5bce499", + "candidate_render_sha256": "1606725273089b83689850ad1c25d1834faa15e00c8cf8aa24e1053273af71bc", + "deployment_uid": "2dbe661a-d232-4477-bbd1-833813077fbe", + "pvc_uid": "9888abeb-9d52-4c70-a23d-b5494780d9c3", + "baseline_spec_sha256": "42e091e52d5dae3c1f986e4898c62057fa80846adfce049a75d8fbd4681180ea", + "candidate_spec_sha256": "c65cb691bab7054ae25de58cdb75933d383621ec4e049d2daa1b1bf52dce0bba", + "old_image": "forgejo.coulomb.social/coulomb/coulomb-social@sha256:522bcd7fce92436765a60c1e4afa2598f3cc40913352153ff1c4319a99ca92ed", + "candidate_image": "forgejo.coulomb.social/coulomb/coulomb-social@sha256:d3f2f5bb90f0ba79f4e1d8098d2eb06f8940b8789e5ecb851914bbab364597ec", + "product_source": "f306dbfa2604e4815523667740be2925c333b84e", + "expected_data": { + "business_tables": 12, + "migration_rows_sha256": "43471516387c0e2a3d35971033ed6af330e2963efe4da93293d97633825d04af", + "business_rows": 93, + "business_rows_sha256": "7953b37b02b80d89ca6869dab6b2b0b996541de20ca0dd1b294a5f25572fb2ca", + "content_files": 86, + "content_files_sha256": "ed535246f4e8f46fcd7ce01f936127996aff873c6936b297f7272194b594aeda", + "global_slug_constraint": true, + "tenant_slug_constraint": true, + "migration_0003_applied": true, + "duplicate_slug_groups": 0 + }, + "public_recipient": "age1qm30jk8q0cezey652dk2dhcr3d9vs528v6lg8mavl6d4q3e3vgnqmrmyy3", + "installed_helper_hashes": { + "/opt/coulomb-social-backup/paired-backup.py": "d161c742d1a6a182460a57d0a1719591ab58fc94006f55169ad3d74442a93630", + "/opt/coulomb-social-backup/snapshot-content-and-db.py": "dcc8dd2a3989f279597595f9260b31b24fcc9b6dc787f04c17ef0491c53d830d", + "/opt/coulomb-social-backup/mirror-paired-backup.py": "9ceb26d9083af016c4b787907e4dce7bdf437c782fb2d15eb28d9e334e6e431c", + "/opt/coulomb-social-backup/check-paired-backup-health.py": "2d40ec6e4c59ea7fe1d46065d197b9a87ca6811f023cc6f3e20e4184a3490566", + "/opt/coulomb-social-backup/backup_capacity.py": "cb699a79e67b129239b5aa5d6f3b0d4ffaa9773a8b073227ad17fdecb5b9d893", + "/opt/railiance-platform/scripts/application_recovery_identities.py": "20f56255ed6eeec4aafe33f142bc3ee03f623d4957a7b0cabb741d340f15f79d", + "/opt/railiance-platform/scripts/application_recovery_mirror.py": "d48ab95e89b9eeae36269d853ebe4c44c22e1f57d4e14224f03d0b9aeeb34178", + "/opt/railiance-platform/scripts/application_recovery_resources.py": "ab70ec341e738272730eb0daf76840d0bca807caeb3c4e4c2357f162fbed4498", + "/opt/railiance-platform/scripts/primary_application_archive.py": "a3f0a61e869a6012edb4b5288be14e3b0c4a9a1273b090320ab146d12e5aa94d", + "/opt/coulomb-social-backup/backup_alerts.py": "07522437d4276b92f0a44149adc6c9e81bb18dbe59ce0f1f9e58a48347700d0e", + "/opt/coulomb-social-backup/notify-backup-alert.py": "0bd53bc5ad22d8bcb0326fb18a17737744b06416fe785dad077308e45f051d5a", + "/opt/coulomb-social-backup/observe-backup-alerts.py": "17caf4fa65283242f618b039ff1d018801c9890ea2a9178912cbdd4b50ee2664" + }, + "clipboard_static_sha256": "347eabe0c8d93bf439df02b5a08a7ea71004600e6ff4e8a42cecbee52a9f0413", + "fresh_backup_before_helm": true, + "requires_dataset_equality_before_and_after": true, + "node_request_percent_limit": 90, + "helm_atomic_timeout_seconds": 300, + "rollback": "Restore Helm revision19/image522bcd7f and verified snapshot pin; preserve all post-capture data. No production data restore or schema downgrade.", + "approval_status": "prepared_not_admitted", + "recovery_rehearsal_scope": "Retained independently recovered 2026-10-31T07:13:52Z point; candidate and previous image preserve all 13 source tables and 86 files. Live observation now has93 business rows versus92 in that point; exact fresh paired backup/readback is required before Helm. Independent restore of the new point remains consumer follow-through. Read-only drift comparison confines differences to sessions and user/member login timestamps; all content and migration history match the retained point.", + "rehearsal_evidence_sha256": { + "content-lifecycle-candidate-build-2026-10-31.json": "83ad2ec41c85f8bb5087a250fdb91126ed92f12351cdc72b353c6f0e1a26452b", + "content-lifecycle-candidate-provenance-2026-10-31.json": "35ed909e0a2fa5c57cea1fcc4fbebb877eae559ca9dc753c3da00f84806d7b73", + "content-lifecycle-candidate-tests-2026-10-31.json": "27a75cfee6bbda41f377b73d6a6c7ecde0c54806c113f1be5223dd2f2cf39b3b", + "content-lifecycle-baseline-tests-2026-10-31.json": "de14e4e10337990ccec1c483f7d943bdc7e11b03aa813aff47d00a9b38a07b65", + "content-lifecycle-candidate-recovery-2026-10-31.json": "8d15172e5e330d01f153336e67f6bffb4363d571530c31ea05db872ca10807a9", + "content-lifecycle-validation-2026-10-31.json": "cbb093af470588ca6e8eb3ec5bc1ccee0be537ab37a46625b6ac267c17343d99", + "content-lifecycle-release-target-2026-10-31.json": "e9b50be3c8284c5a9ae96d75b800cbc4090580fa811c2ecd098d73ce8cbee74e", + "content-lifecycle-release-row-drift-2026-10-31.json": "b10f65bf55e900199ae6037767f32e8901aaccb6f7f3a86461f13dd02ec8efce" + }, + "new_resources_committed": 0, + "autopilot_or_eur_budget_claimed": false, + "values_file": "release/content-lifecycle-values.yaml", + "snapshot_file": "release/content-lifecycle-snapshot.py", + "evidence_directory": "release/content-lifecycle-evidence", + "prerequisite_installation_digest": "sha256:8f442bf0ebed8bbf3b7f227b52081df52613f3f5f6874c4a59511cfc2ba06d42", + "requires_inactive_backup_units": true, + "files": { + "tools/coulomb-image-release.py": "4cefb7b4ce7fb7eece1cd78ce3c1712a2b5cc2f9c81acfed06e3c6ac7c4abad3", + "tools/coulomb_image_release_node.py": "5547b3809f34178e48404cd1590f665729b062b4333f561e2da81b56d435dece", + "tools/coulomb_image_probe.py": "7d241b288992fbbc961b42a1311f7d0938b0c899c0675b7db92653c5ace6014f", + "release/content-lifecycle-values.yaml": "daa302f96bddb889c7fe4aa95b740e9f5184d2ac463cd2403431b3e94db86528", + "release/content-lifecycle-snapshot.py": "c1508043f473d2762723acda04944a36f5346e7641fbe6fdf21a91acd3921a54", + "charts/coulomb-social/Chart.yaml": "663d6c23c57457a03bc376ebafb8107ee7f150e150f4d4fb6a3a949fb21006aa", + "charts/coulomb-social/templates/_helpers.tpl": "65379818d484959e7221d3f538b59ffd6b8b7d3c9493250e47520576aee1fc68", + "charts/coulomb-social/templates/content-pvc.yaml": "5c4ddcaee2b3dcdf17da061b1a7a2cb1e0398d2f56f4ef31790cfaaf648b0fee", + "charts/coulomb-social/templates/deployment.yaml": "f90b7517959ec8785f578f917fec222287931e4f8a27f6f9e8fc0a28385f7df8", + "charts/coulomb-social/templates/service.yaml": "78ce0ec6990c5ad57703c7a8d5d2c88c8eb9302d325cb2ba9d2df34e5f90ebdd", + "charts/coulomb-social/values.yaml": "af48d133f7fe45f0d762b40878f306ef48262f6cbbca61808ffac5dd9fc9d241", + "release/content-lifecycle-evidence/content-lifecycle-baseline-tests-2026-10-31.json": "de14e4e10337990ccec1c483f7d943bdc7e11b03aa813aff47d00a9b38a07b65", + "release/content-lifecycle-evidence/content-lifecycle-candidate-build-2026-10-31.json": "83ad2ec41c85f8bb5087a250fdb91126ed92f12351cdc72b353c6f0e1a26452b", + "release/content-lifecycle-evidence/content-lifecycle-candidate-provenance-2026-10-31.json": "35ed909e0a2fa5c57cea1fcc4fbebb877eae559ca9dc753c3da00f84806d7b73", + "release/content-lifecycle-evidence/content-lifecycle-candidate-recovery-2026-10-31.json": "8d15172e5e330d01f153336e67f6bffb4363d571530c31ea05db872ca10807a9", + "release/content-lifecycle-evidence/content-lifecycle-candidate-tests-2026-10-31.json": "27a75cfee6bbda41f377b73d6a6c7ecde0c54806c113f1be5223dd2f2cf39b3b", + "release/content-lifecycle-evidence/content-lifecycle-release-row-drift-2026-10-31.json": "b10f65bf55e900199ae6037767f32e8901aaccb6f7f3a86461f13dd02ec8efce", + "release/content-lifecycle-evidence/content-lifecycle-release-target-2026-10-31.json": "e9b50be3c8284c5a9ae96d75b800cbc4090580fa811c2ecd098d73ce8cbee74e", + "release/content-lifecycle-evidence/content-lifecycle-validation-2026-10-31.json": "cbb093af470588ca6e8eb3ec5bc1ccee0be537ab37a46625b6ac267c17343d99" + } +} diff --git a/docs/release/content-lifecycle-release-row-drift-2026-10-31.json b/docs/release/content-lifecycle-release-row-drift-2026-10-31.json new file mode 100644 index 0000000..8c8aeec --- /dev/null +++ b/docs/release/content-lifecycle-release-row-drift-2026-10-31.json @@ -0,0 +1,61 @@ +{ + "changed_columns": { + "django_session": [ + "session_key", + "session_data", + "expire_date" + ], + "members_member": [ + "updated_at", + "last_login_at" + ], + "members_user": [ + "last_login" + ] + }, + "file_changes": [], + "retained_fingerprint": { + "business_tables": 12, + "migration_rows_sha256": "43471516387c0e2a3d35971033ed6af330e2963efe4da93293d97633825d04af", + "business_rows": 92, + "business_rows_sha256": "6243955479f7861fcdcd79b62f7f1e9f9a27c05a17bd2c17b792ebc98b2fa7b6", + "content_files": 86, + "content_files_sha256": "ed535246f4e8f46fcd7ce01f936127996aff873c6936b297f7272194b594aeda", + "global_slug_constraint": true, + "tenant_slug_constraint": true, + "migration_0003_applied": true, + "duplicate_slug_groups": 0 + }, + "schema": "coulomb.content-release-row-drift.v1", + "changed_tables": [ + { + "table": "django_session", + "retained_rows": 42, + "current_rows": 43, + "retained_sha256": "c958e33ec782f9f2fd1b3cceba4a64da0038fde51050f25905ae42de91ca6dde", + "current_sha256": "594e246ef6297d9be9b6e8d9eda076a5b2ab36afb9263606b719f64f51293ba6" + }, + { + "table": "members_member", + "retained_rows": 1, + "current_rows": 1, + "retained_sha256": "4d465d2b9d1e6311e505f861f2ed4b67763a545a965f9a7d0c586a3d07e3e9a3", + "current_sha256": "ab82b3f2c5e49f8634c598c9642894747f4b6fcff0f5230ac27f41939feb8640" + }, + { + "table": "members_user", + "retained_rows": 1, + "current_rows": 1, + "retained_sha256": "81ccb3a40191e910a68c101896df518a588c00231728934c4c900577f8ae4f12", + "current_sha256": "0fc7a71b4b80a71fbedce527bbd2cae3858cf6a062e5714fc231be13ff987256" + } + ], + "all_other_business_tables_equal": true, + "migration_history_equal": true, + "content_equal": true, + "retained_point": "2026-10-31T07:13:52Z", + "current_business_rows": 93, + "current_business_rows_sha256": "7953b37b02b80d89ca6869dab6b2b0b996541de20ca0dd1b294a5f25572fb2ca", + "private_row_values_printed": false, + "production_changes": false +} diff --git a/docs/release/content-lifecycle-release-target-2026-10-31.json b/docs/release/content-lifecycle-release-target-2026-10-31.json new file mode 100644 index 0000000..a3e6e05 --- /dev/null +++ b/docs/release/content-lifecycle-release-target-2026-10-31.json @@ -0,0 +1,62 @@ +{ + "schema": "coulomb.content-release-target-observation.v1", + "observed_at": "2026-10-31T12:07:24.322343+00:00", + "cluster_uid": "a553c742-0115-43d4-99a4-a5ca56fe0786", + "deployment_uid": "2dbe661a-d232-4477-bbd1-833813077fbe", + "pvc_uid": "9888abeb-9d52-4c70-a23d-b5494780d9c3", + "baseline_revision": 19, + "baseline_helm_manifest_sha256": "93cb277abd1aea0657be482e458dcfb268fe0bfbd45d90aa1faedcc7f5bce499", + "baseline_spec_sha256": "42e091e52d5dae3c1f986e4898c62057fa80846adfce049a75d8fbd4681180ea", + "candidate_spec_sha256": "c65cb691bab7054ae25de58cdb75933d383621ec4e049d2daa1b1bf52dce0bba", + "old_image": "forgejo.coulomb.social/coulomb/coulomb-social@sha256:522bcd7fce92436765a60c1e4afa2598f3cc40913352153ff1c4319a99ca92ed", + "candidate_image": "forgejo.coulomb.social/coulomb/coulomb-social@sha256:d3f2f5bb90f0ba79f4e1d8098d2eb06f8940b8789e5ecb851914bbab364597ec", + "sole_ready_pvc_writer": true, + "existing_unmanaged_metadata_preserved": true, + "candidate_image_only_server_preview_verified": true, + "data": { + "business_tables": 12, + "migration_rows_sha256": "43471516387c0e2a3d35971033ed6af330e2963efe4da93293d97633825d04af", + "business_rows": 93, + "business_rows_sha256": "7953b37b02b80d89ca6869dab6b2b0b996541de20ca0dd1b294a5f25572fb2ca", + "content_files": 86, + "content_files_sha256": "ed535246f4e8f46fcd7ce01f936127996aff873c6936b297f7272194b594aeda", + "global_slug_constraint": true, + "tenant_slug_constraint": true, + "migration_0003_applied": true, + "duplicate_slug_groups": 0, + "actual_owner_pages_rendered": 40, + "unsaved_outsider_page_denials": 40, + "production_identity_or_grant_created": false + }, + "matches_verified_revision19_data": false, + "installed_helper_sha256": { + "/opt/coulomb-social-backup/paired-backup.py": "43e119acb6a4933b4e144c349a091e074e1a91ab61dbe15a65c8264c1d47661a", + "/opt/coulomb-social-backup/snapshot-content-and-db.py": "dcc8dd2a3989f279597595f9260b31b24fcc9b6dc787f04c17ef0491c53d830d", + "/opt/railiance-platform/scripts/primary_application_archive.py": "a3f0a61e869a6012edb4b5288be14e3b0c4a9a1273b090320ab146d12e5aa94d" + }, + "timers": { + "coulomb-social-paired-backup.timer": { + "LoadState": "not-found", + "ActiveState": "inactive", + "UnitFileState": "" + }, + "coulomb-social-backup-freshness.timer": { + "LoadState": "not-found", + "ActiveState": "inactive", + "UnitFileState": "" + } + }, + "prerequisites": { + "custody": { + "exists": false + }, + "installation": { + "exists": false + }, + "route": { + "exists": false + } + }, + "production_data_modified": false, + "release_executed": false +} diff --git a/docs/release/corpus-import-inspection-2026-10-31.md b/docs/release/corpus-import-inspection-2026-10-31.md new file mode 100644 index 0000000..dc5c1f1 --- /dev/null +++ b/docs/release/corpus-import-inspection-2026-10-31.md @@ -0,0 +1,90 @@ +# Inspecting uncertain corpus imports + +CSOC-WP-0014-T07 preparation. The receipt-backed importer now reserves the new +Space UUID in its durable `started` checkpoint before import writes. Its SQL +operation creates that exact UUID exclusively. A competing row at the slug or +reserved UUID refuses instead of being updated or receiving imported grants. +The committed return must still have the reserved identity before completion. +Ordinary imports without receipt mode retain their existing behavior. + +This closes an identity gap in the earlier checkpoint implementation: a started +receipt previously named no intended SQL identity, and the underlying generic +importer could update an existing same-tenant row when its content tree was absent. +Legacy completed checkpoints remain usable. Legacy started checkpoints remain +uncertain and are reported as identity-unbound; no identity is invented for them. + +## Read-only inspection + +The new management command is: + +```sh +python manage.py inspect_content_corpus /private/reviewed/manifest.json \ + --receipt-dir /private/import-attempt +``` + +It requires the original manifest/source/mapping/destination binding and an +existing checkpoint and lock files. It does not create missing directories or +locks. It takes the existing importer/content locks without invoking content +recovery, then reads SQL in one transaction. PostgreSQL uses `REPEATABLE READ, +READ ONLY`. It compares destination and checkpoint bookends and rechecks source +hashes before reporting. These cooperating locks are **not** proof of an external +writer fence; the report explicitly records that limit. + +Output contains unit indexes, classifications, counts and hashes, without source +bodies, identity names or SQL rows. It binds the checkpoint bytes and observed SQL, +grants and files. Classifications distinguish: + +- A reserved identity whose current source-derived metadata, mapped grants and + complete source file hashes match. +- A reserved identity with differences, including later content, metadata, grant + or original-date changes. +- Foreign/replacement identities, files without SQL, or SQL without files. +- No SQL row or published tree, which does not establish absence of staging. +- Older started receipts without a reserved identity. +- Pending destinations and the continued presence of completed identities. + +Original space dates are compared when supplied by the source index; absence is +reported. Insertion/update timestamps that cannot be derived from source are +fingerprinted, not fabricated. A matching result does not prove that no intervening +edit ever occurred. Synthesized indexes or omitted extra source files cannot be +called byte-exact by the whole-tree comparison. Completed units may have later +edits; their original receipt remains historical evidence, not an overwrite target. +Unattributed `.import-*` entries are counted, never assigned to an attempt or removed. + +Every result explicitly says that no checkpoint was completed, no retry was +authorized and no data was restored. The inspector does not convert `started` to +`completed` or `pending`. The deployment owner must still reconcile the specific +uncertain state with its retained backup, writer fence and reviewed operation. +Do not remove the checkpoint or use replacement import to bypass uncertainty. + +The subsequent [matching-commit procedure](corpus-import-reconciliation-2026-10-31.md) +adds a separate, explicit checkpoint operation for one exactly matching reserved +commit. Inspection itself remains read-only. Partial, changed, unbound and +unattributed-staging states still require owner reconciliation. + +## Verification and release boundary + +Fifteen new cases cover matching committed state without receipt, partial stores, +legacy/unrelated identities, later edits/grants, original dates, active locks, +missing paths, changing observations and occupied-slug refusal. The existing real +process-exit tests now inspect both the pre-write and committed-without-receipt +states while proving unchanged checkpoints. A row inserted after corpus preflight +is retained without imported grants or destination files. + +Full `make check`: **629 passed, 69 existing warnings, zero failures/errors/skips +in 71.11s**, including the required browser journeys. Ruff and whitespace checks +pass. Log: `/tmp/csoc-import-inspection-check-20261031.log`. + +`scripts/rehearse-import-inspection.py` also runs the corpus, receipt and inspection +tests against disposable PostgreSQL 16.14 on an internal Docker network with no +published ports. It passed 60 checks of actual inspection transactions' read-only +and repeatable-read modes. Current source and locked development dependencies are explicit +read-only overlays on the cached application image; this is not packaged-candidate +or production proof. Its containers, anonymous test volumes and network were +removed. Source-bound receipt: +`corpus-import-inspection-postgres-2026-10-31.json`. + +No live cohort, grant, deployment, release pin or previously reviewed executor +changed. T07 remains in progress: source/identity/grant reconciliation, a concrete +owner procedure for uncertain imports, supervised executor integration, release, +fresh backup/fencing, native postflight and productive acceptance remain required. diff --git a/docs/release/corpus-import-inspection-postgres-2026-10-31.json b/docs/release/corpus-import-inspection-postgres-2026-10-31.json new file mode 100644 index 0000000..6c7a1c1 --- /dev/null +++ b/docs/release/corpus-import-inspection-postgres-2026-10-31.json @@ -0,0 +1,28 @@ +{ + "schema": "coulomb.corpus-inspection-rehearsal.v1", + "proof": { + "status": "passed", + "database_vendor": "postgresql", + "server_version": 160014, + "inspection_transaction_mode_checks": 60, + "source_overlay": true, + "locked_development_dependencies": true, + "production_data_used": false + }, + "app_dependency_image": "sha256:522bcd7fce92436765a60c1e4afa2598f3cc40913352153ff1c4319a99ca92ed", + "postgres_image": "sha256:ff23cdce56cac62ada6f66013e1a50864c0abbe79d132d40b6e05bd80f378a70", + "internal_network": true, + "published_ports": false, + "disposable_resources_removed": true, + "source_sha256": { + "scripts/rehearse-import-inspection.py": "2f560e25f00b08cdbcf1a27440c02fc74c57d255429b0496e3f4e25b7eb90468", + "uv.lock": "e8ebb6fc4b5abd1ac80f1c9012f1983642d0528a8a213cfcc394261606fad887", + "coulomb_social/apps/spaces/import_tree.py": "f232bde0d1741668ff28a6cb24306b7f67d8bf9fa3838507a8db4f4e80361784", + "coulomb_social/apps/spaces/import_receipts.py": "670a5eb48613df47bf8fd0cf7d9306f7d2e992d46dff69680e6e7998f64f9cb2", + "coulomb_social/apps/spaces/import_inspection.py": "627f7c006f6aedf337cd146b1052028afe8342ce820283cdfb7963266d9e754d", + "coulomb_social/apps/spaces/management/commands/inspect_content_corpus.py": "b083e39aa766c8ba268788fae98507780604d40832e119db1cd9f7c10f1aa212", + "tests/test_import_corpus.py": "5a0670c408740de86ce601cd9d2a0a12cf0aaa5afda416867605180c568a1c5f", + "tests/test_import_receipts.py": "134e8901991dc493333b7543bbd6ff1f1e7b7ca3d5cf9d7849d7c45212e20aab", + "tests/test_import_inspection.py": "890e783eec30dd1b9e93ad7c3ab3173b57174a096938520865eaaf420bc605fb" + } +} diff --git a/docs/release/corpus-import-receipts-2026-10-31.md b/docs/release/corpus-import-receipts-2026-10-31.md new file mode 100644 index 0000000..737a829 --- /dev/null +++ b/docs/release/corpus-import-receipts-2026-10-31.md @@ -0,0 +1,79 @@ +# Restartable corpus import checkpoints + +WP-0014-T07 preparation, 2026-10-31. The existing corpus importer committed one +space at a time, but a whole-manifest retry failed when an earlier destination +already existed. Its ordinary-error rollback did not constitute crash recovery. + +`import_content_corpus --receipt-dir ` now supports durable +boundaries between new-space imports. Existing commands without this option retain +their behavior. Receipt mode refuses `--replace` and any unreceipted SQL row or +content destination, even when only one of those stores exists. The entire corpus +is preflighted before import writes. Dry-run does not create content or receipts. + +## Receipt contract + +- A checkpoint binds the exact manifest bytes/order, source tree hashes, resolved + source paths, explicit mapped member IDs/roles, database configuration identity + and content root. It neither discovers identities nor grants approval. Plan order + is preserved; the reconciled manifest must supply the agreed LIFO order. +- The receipt directory must be owned by the operator, private and separate from + source and served content. Symlinks, shared permissions and hardlinked receipt + files are refused. Directory mode is0700 and files0600. A nonblocking lock stops + simultaneous use of one receipt directory; the existing content lock serializes + cooperating content writers. +- Each space moves from pending to started before import writes. On success, the + helper verifies the mapped grants, fsyncs content files/directories, then writes + an atomic, fsynced completed checkpoint containing the stable Space ID, content, + metadata and grant hashes and page/asset counts. It refuses an enclosing database + transaction, so a completed receipt cannot precede an outer rollback. + The later inspection follow-through also reserves the new Space UUID before + writes and uses exclusive SQL creation; see the linked review below. +- A retry checks the binding and skips completed spaces with the same stable SQL + identity. Later body, title and grant edits remain intact. A deleted or replaced + identity requires reconciliation. Historical completion hashes describe the + original return; they do not claim the current edited space is unchanged. +- A started checkpoint always stops automatic retry, including after a lost commit + reply or failed receipt write. A crash between the filesystem and SQL operations + may leave partial content or staging directories. This change does not implement + automatic repair within a space. Do not delete the checkpoint, use `--replace`, + or label it completed merely to bypass that stop. + +## Execution boundary + +Only synthetic isolated fixtures were imported for this change. No deployment, +production import, grant or release-packet change occurred. The previously approved +single-cohort executor is unchanged and does not use this new option. + +Before remaining-cohort execution, the deployment owner must integrate the helper +into the existing supervised path with immutable reviewed sources, explicit mappings, +external writer fencing, a fresh paired backup and native postflight. The cooperating +content lock is not an external writer fence, and a database-configuration hash is +not independent cluster identity proof. The owner must mount a durable private +receipt directory outside served content and include it in recovery custody. + +If an attempt leaves a started checkpoint, keep the relevant writers fenced, +retain the checkpoint, staged trees and paired backup, and inspect the actual SQL +and files. Any reconciliation/restore needs its concrete reviewed procedure; the +helper supplies no generic automatic repair or permission to restore production. +Source fidelity, original-date verification, assets/links and productive transfer +verdicts remain the existing workplan requirements. + +Read-only classification is now available through `inspect_content_corpus`. +It distinguishes exact reserved identities from partial, changed, foreign and +legacy identity-unbound outcomes without editing a checkpoint or authorizing a +retry. See [inspection review](corpus-import-inspection-2026-10-31.md). + +## Verification + +The new synthetic suite exercises real process termination before import, after +SQL commit but before a completed receipt, and after the first completed space. +The completed-boundary retry preserves a later local edit and stable Space ID while +importing the next space. Other checks cover changed source/manifest/destination, +mapping refusal, existing SQL/content, private-path constraints, deleted identities, +concurrent receipt use, enclosing SQL transactions and receipt-write failure. + +Full `make check`: **614 passed**,69warnings,zero failures/errors/skips in71.32s, +including the required browser journeys and all18new checkpoint cases. Log: +`/tmp/csoc-import-receipts-check-20261031.log`. State Hub synchronization passed +with8warnings and0errors; T07 reads back as progress. Log: +`/tmp/csoc-import-receipts-consistency-20261031.log`. diff --git a/docs/release/corpus-import-reconciliation-2026-10-31.md b/docs/release/corpus-import-reconciliation-2026-10-31.md new file mode 100644 index 0000000..af176f1 --- /dev/null +++ b/docs/release/corpus-import-reconciliation-2026-10-31.md @@ -0,0 +1,96 @@ +# Completing a matching uncertain import checkpoint + +CSOC-WP-0014-T07 now has an explicit checkpoint-completion operation for an +already committed import whose reserved identity and complete source-derived +state match. This is source preparation, not a production reconciliation or +admission of any cohort. The existing native executor and frozen release packets +are unchanged. + +## Owner procedure + +Before any native use, the deployment owner must identify the exact original +attempt, stop and independently verify application and external writers, retain +the original checkpoint and a coherent paired recovery point, and confirm the +source/tenant/identity/grant mapping was admitted. Neither of the commands below +verifies an external writer fence or provider backup. The current production +image does not contain these new commands; owner integration and release remain +required. + +1. Inspect the original manifest and existing private receipt directory using + `inspect_content_corpus`. Keep the result privately with the operation evidence. +2. Review the selected zero-based unit index, reserved identity, source binding, + complete file/grant/metadata comparison and the original operation's outcome. + Only `reserved_destination_matches_source`, with no unattributed `.import-*` + entries, is eligible for the operation below. A match describes current state; + it is not proof that no intervening edit ever occurred. +3. Supply the exact inspection SHA-256 and explicit completion action: + + ```sh + python manage.py reconcile_content_import /private/reviewed/manifest.json \ + --receipt-dir /private/import-attempt \ + --unit-index 0 \ + --expect-inspection \ + --complete-matching-commit + ``` + + The command reacquires the existing receipt/content locks and a consistent + read-only SQL transaction, recomputes the whole inspection, and refuses a + changed digest. It fsyncs the matching destination, rechecks the selected unit, + source and checkpoint bytes, then atomically replaces only the checkpoint. + The completed unit retains its original reserved identity, ordinary importer + fingerprints/counts, prior started unit and full hash/count inspection evidence. + SQL rows, grants and content bytes are not modified. +4. Independently read the checkpoint and inspect again. A lost response or failure + may occur after replacement; do not infer failure to complete from the absence + of terminal output. The previous inspection cannot be replayed against an + already completed checkpoint. An interrupted temporary checkpoint file is + retained as evidence and is not automatically deleted or adopted. +5. If continuation is separately admitted, the existing receipted importer skips + the completed unit and preserves later edits and grants. Completion of this + checkpoint does not authorize importing the remaining corpus. + +## Other states remain explicit owner work + +| Observed state | Required disposition | +| --- | --- | +| Reserved identity differs from source | Preserve later edits/grants; reconcile against the original operation and paired point. This command never overwrites them. | +| SQL only, files only, or neither | Retain the uncertain checkpoint and inspect private staging, transaction outcome and the paired recovery point. No automatic reset to pending, retry, restore or deletion is implemented here. | +| Foreign identity or legacy unbound started receipt | Resolve ownership and identity from independent evidence. Do not adopt a row because its slug matches. | +| Unattributed staging entries | Establish their exact attempt/ownership and disposition separately. Matching published bytes do not authorize cleaning up those entries. | +| Pending or already completed unit | No reconciliation mutation is permitted. Completed identities remain historical boundaries and may have valid later edits. | + +The helper uses cooperating file locks and PostgreSQL repeatable-read/read-only +transactions. These are not an external writer fence. The return explicitly +reports that limit, no SQL/content changes, no restore, and no authorization to +continue imports. The owner must verify native prerequisites before execution; +passing synthetic tests does not supply them. + +## Validation + +The focused suite covers exact completion and evidence retention, unchanged SQL/ +content, preservation of later edits on resumed import, stale evidence, changed +source/mapping/checkpoint, partial or foreign stores, legacy receipts, unattributed +staging, invalid selection, sync failure and drift during fsync. Real child-process +exits immediately before and after checkpoint replacement leave either the prior +started checkpoint or the completed checkpoint with its reconciliation evidence. + +Full `make check`: **648 passed, 69 existing warnings, zero failures/errors/skips +in 150.38s**, including required browser journeys, rerun after the interrupted +session. Nineteen new reconciliation +cases pass; the focused corpus checkpoint/inspection/reconciliation selection +passes all 52 cases. Log: `/tmp/csoc-import-reconciliation-check-20261031.log`. + +The isolated PostgreSQL 16.14 rehearsal passes with 243 actual read-only and +repeatable-read transaction checks. It uses synthetic data, explicit source and +locked dependency overlays, an internal Docker network and no published ports. +Its disposable containers, volumes and network were removed. Source-bound receipt: +`corpus-import-reconciliation-postgres-2026-10-31.json`. This is not packaged +candidate or production proof. + +After resuming, all 12 source hashes in that PostgreSQL receipt were independently +checked against the current files and matched. State Hub consistency passed and +the T07 description was independently read back with the reconciliation evidence. + +T07 remains in progress for the other reconciliation states, supervised owner +integration, source/grant admission, native release/import/postflight and +productive verdicts. diff --git a/docs/release/corpus-import-reconciliation-postgres-2026-10-31.json b/docs/release/corpus-import-reconciliation-postgres-2026-10-31.json new file mode 100644 index 0000000..a5362b5 --- /dev/null +++ b/docs/release/corpus-import-reconciliation-postgres-2026-10-31.json @@ -0,0 +1,31 @@ +{ + "schema": "coulomb.corpus-inspection-rehearsal.v1", + "proof": { + "status": "passed", + "database_vendor": "postgresql", + "server_version": 160014, + "inspection_transaction_mode_checks": 243, + "source_overlay": true, + "locked_development_dependencies": true, + "production_data_used": false + }, + "app_dependency_image": "sha256:522bcd7fce92436765a60c1e4afa2598f3cc40913352153ff1c4319a99ca92ed", + "postgres_image": "sha256:ff23cdce56cac62ada6f66013e1a50864c0abbe79d132d40b6e05bd80f378a70", + "internal_network": true, + "published_ports": false, + "disposable_resources_removed": true, + "source_sha256": { + "scripts/rehearse-import-inspection.py": "34a95a57df6b93132a45c70f95b56b8ee9ef5be8677efa6f27f48c41bdc46f17", + "uv.lock": "e8ebb6fc4b5abd1ac80f1c9012f1983642d0528a8a213cfcc394261606fad887", + "coulomb_social/apps/spaces/import_tree.py": "f232bde0d1741668ff28a6cb24306b7f67d8bf9fa3838507a8db4f4e80361784", + "coulomb_social/apps/spaces/import_receipts.py": "670a5eb48613df47bf8fd0cf7d9306f7d2e992d46dff69680e6e7998f64f9cb2", + "coulomb_social/apps/spaces/import_inspection.py": "908a62bd31e0a6cfab3cc94f45c671ff4f1ec403035434790a29adfc5275f5ab", + "coulomb_social/apps/spaces/import_reconciliation.py": "75abc50ec1c6e15a12534eb39edc5ad3fa4128c37a354d4cea2bf6c37f745771", + "coulomb_social/apps/spaces/management/commands/inspect_content_corpus.py": "b083e39aa766c8ba268788fae98507780604d40832e119db1cd9f7c10f1aa212", + "coulomb_social/apps/spaces/management/commands/reconcile_content_import.py": "2e25d94ab3a9659e38a3ea4c8b6c2e87c710216ab5834a248fccdd00da9fdbb9", + "tests/test_import_corpus.py": "5a0670c408740de86ce601cd9d2a0a12cf0aaa5afda416867605180c568a1c5f", + "tests/test_import_receipts.py": "134e8901991dc493333b7543bbd6ff1f1e7b7ca3d5cf9d7849d7c45212e20aab", + "tests/test_import_inspection.py": "890e783eec30dd1b9e93ad7c3ab3173b57174a096938520865eaaf420bc605fb", + "tests/test_import_reconciliation.py": "89d7161715a2c298c30c6ae38f8d9647688e2ea261d2913a8a9b8969202ac298" + } +} diff --git a/docs/release/editor-attachments-2026-10-31.sources.txt b/docs/release/editor-attachments-2026-10-31.sources.txt new file mode 100644 index 0000000..8181ea7 --- /dev/null +++ b/docs/release/editor-attachments-2026-10-31.sources.txt @@ -0,0 +1,6 @@ +/home/worsch/.config/railiance-platform/coulomb-social-recovery/content-point-recovery-20261031/csoc-verify-deployed-editor.py: f5ecee55accd7653be1cdf7402dd85185d6fd6305bf676f7ce00c63ec6259bd8 +/home/worsch/.config/railiance-platform/coulomb-social-recovery/content-point-recovery-20261031/csoc-editor-target.py: 945ae15fbc328e537ff41b778b92bfcee73754983546149cbbbd2b1503dfc6d6 +/home/worsch/.config/railiance-platform/coulomb-social-recovery/content-point-recovery-20261031/csoc-verify-editor-package.py: 2c877018ffd329ec3f3c9a345aba4ee04608d6f21ba5eb372bebad53352bd58e +/home/worsch/.config/railiance-platform/coulomb-social-recovery/content-point-recovery-20261031/csoc-verify-editor-recovery.py: 29c53d551eec2b4f4d60b047f123c727eef76ee7677c054bbfb0ab008355cff9 +/home/worsch/.config/railiance-platform/coulomb-social-recovery/content-point-recovery-20261031/csoc-editor-baseline-diff.py: 1680f58e00e78dbd50b2a45e2a0d006acc94c073fe7cfac27436647a902d34bd +/home/worsch/.config/railiance-platform/coulomb-social-recovery/content-point-recovery-20261031/csoc-build-editor.py: 27a07652118d5d42cc99f258f6dc2580eb84cbf8b7591b88c08f92a3c3c1dfcf diff --git a/docs/release/editor-attachments-baseline-change-2026-10-31.json b/docs/release/editor-attachments-baseline-change-2026-10-31.json new file mode 100644 index 0000000..71dc2f7 --- /dev/null +++ b/docs/release/editor-attachments-baseline-change-2026-10-31.json @@ -0,0 +1,39 @@ +{ + "schema": "coulomb.acceptance-baseline-change.v1", + "retained_point": "2026-10-31T20:21:01Z", + "old_business_rows": 93, + "new_business_rows": 97, + "table_changes": { + "spaces_space": { + "old": 2, + "new": 4, + "removed": 0, + "added": 2 + }, + "spaces_spacemembership": { + "old": 1, + "new": 3, + "removed": 0, + "added": 2 + } + }, + "old_content_files": 86, + "new_content_files": 98, + "old_files_missing": 0, + "old_files_changed": 0, + "new_files_added": 12, + "old_rows_preserved": true, + "current_baseline": { + "business_tables": 12, + "migration_rows_sha256": "43471516387c0e2a3d35971033ed6af330e2963efe4da93293d97633825d04af", + "business_rows": 97, + "business_rows_sha256": "35b0ae7d9dd1ff21c2ceaa04649162545ec36a2c68629548238352d9cd0fb303", + "content_files": 98, + "content_files_sha256": "b657c0e470399b6c7500da500cbbec061bea12ab9633bd74e2f142d797b2d26a", + "global_slug_constraint": true, + "tenant_slug_constraint": true, + "migration_0003_applied": true, + "duplicate_slug_groups": 0 + }, + "interpretation": "Changes observed during the user-requested productive acceptance session; no user verdict inferred." +} diff --git a/docs/release/editor-attachments-build-2026-10-31.json b/docs/release/editor-attachments-build-2026-10-31.json new file mode 100644 index 0000000..30efe22 --- /dev/null +++ b/docs/release/editor-attachments-build-2026-10-31.json @@ -0,0 +1,45 @@ +{ + "schema": "coulomb.content-acceptance-build.v1", + "completed_at": "2026-10-31T21:13:04.039838+00:00", + "source": "31eaa581a65e454b6ef9979326c5bdfe10e13806", + "image": "forgejo.coulomb.social/coulomb/coulomb-social@sha256:b58d858abd5b4acbd6b893c3f622f7a9fdb5e606164ab9866eb3cfaa04df1a0e", + "build_context": "clean restricted git archive", + "source_full_gate_tests": 568, + "runtime_packages_and_python_unchanged": true, + "runtime": { + "python": "3.12.16", + "distributions": { + "h11": "0.16.0", + "httpcore": "1.0.9", + "psycopg": "3.3.4", + "joserfc": "1.7.4", + "anyio": "4.14.2", + "cryptography": "50.0.0", + "idna": "3.18", + "certifi": "2026.7.22", + "packaging": "26.3", + "python-decouple": "3.8", + "psycopg-binary": "3.3.4", + "sqlparse": "0.5.5", + "whitenoise": "6.12.0", + "cffi": "2.1.1", + "markdown": "3.10.3", + "webencodings": "0.5.1", + "pycparser": "3.0", + "bleach": "6.4.0", + "authlib": "1.7.2", + "django": "6.1", + "typing_extensions": "4.16.0", + "gunicorn": "26.0.0", + "asgiref": "3.12.1", + "httpx": "0.28.1", + "dj-database-url": "3.1.2" + } + }, + "django_checks": { + "check": "System check identified no issues (0 silenced).", + "makemigrations": "No changes detected" + }, + "new_database_migrations": false, + "production_deployed": false +} diff --git a/docs/release/editor-attachments-fresh-client-export-2026-11-01.json b/docs/release/editor-attachments-fresh-client-export-2026-11-01.json new file mode 100644 index 0000000..5ee5ec7 --- /dev/null +++ b/docs/release/editor-attachments-fresh-client-export-2026-11-01.json @@ -0,0 +1,72 @@ +{ + "schema": "coulomb.packaged-content-acceptance-recovery.v1", + "status": "packaged_recovered_export_and_views_verified", + "source": "31eaa581a65e454b6ef9979326c5bdfe10e13806", + "image": "forgejo.coulomb.social/coulomb/coulomb-social@sha256:b58d858abd5b4acbd6b893c3f622f7a9fdb5e606164ab9866eb3cfaa04df1a0e", + "restore_receipt_sha256": "1a696920d4cce738553ad536b62820e2bfda01cc89a7bd3d3eb8d5ceb3e266bb", + "retained_point_captured_at": "2026-10-31T23:29:54Z", + "candidate_application": { + "business_tables": 12, + "migration_rows_sha256": "43471516387c0e2a3d35971033ed6af330e2963efe4da93293d97633825d04af", + "business_rows": 97, + "business_rows_sha256": "35b0ae7d9dd1ff21c2ceaa04649162545ec36a2c68629548238352d9cd0fb303", + "content_files": 98, + "content_files_sha256": "b657c0e470399b6c7500da500cbbec061bea12ab9633bd74e2f142d797b2d26a", + "global_slug_constraint": true, + "tenant_slug_constraint": true, + "migration_0003_applied": true, + "duplicate_slug_groups": 0, + "actual_owner_pages_rendered": 43, + "unsaved_outsider_page_denials": 43, + "production_identity_or_grant_created": false + }, + "previous_image_application": { + "business_tables": 12, + "migration_rows_sha256": "43471516387c0e2a3d35971033ed6af330e2963efe4da93293d97633825d04af", + "business_rows": 97, + "business_rows_sha256": "35b0ae7d9dd1ff21c2ceaa04649162545ec36a2c68629548238352d9cd0fb303", + "content_files": 98, + "content_files_sha256": "b657c0e470399b6c7500da500cbbec061bea12ab9633bd74e2f142d797b2d26a", + "global_slug_constraint": true, + "tenant_slug_constraint": true, + "migration_0003_applied": true, + "duplicate_slug_groups": 0, + "actual_owner_pages_rendered": 43, + "unsaved_outsider_page_denials": 43, + "production_identity_or_grant_created": false + }, + "client_exports": [ + { + "tenant_sha256": "98dd71602ef1afc56f1dcbaa7f5517198fcdd093a57d823ba11e8a634ea58ec2", + "status": "recovered_client_export_verified", + "schema": "coulomb.client-dataset-export.v1", + "content_files": 97, + "records": { + "members.user": 1, + "members.member": 1, + "spaces.space": 3, + "spaces.spacemembership": 3 + }, + "admin_events": 0, + "archive_sha256": "a4eb3daa1ff3427ff71eeaeb415b9ce645d84a65d81afbb0470d47478bc58b6a", + "offboarding_complete": false, + "all_source_tables_compared": 13, + "all_source_files_compared": 98, + "archive_member_hashes_verified": true, + "selected_client_content_complete_locally": true, + "source_sql_and_content_unchanged": true, + "external_bindings_require_review": 0, + "sessions_and_credentials_excluded": true + } + ], + "exported_tenant_count": 1, + "source_content_files_unchanged": 98, + "all_tables_unchanged": true, + "application_source_overlay": false, + "network_internal": true, + "published_ports": 0, + "production_access": false, + "production_restore": false, + "driver_sha256": "5bfac786cbbf64bdbbd5b4b19d9fddfe0f99a28499417dbbbc0c2ca74e2cc69a", + "verification_query_note": "Deduplicate tenant IDs in memory: Space ordering by title/slug caused values_list(...).distinct() to repeat the same tenant across spaces. The corrected check exports each actual content tenant; this point has one." +} diff --git a/docs/release/editor-attachments-fresh-recovery-2026-11-01.json b/docs/release/editor-attachments-fresh-recovery-2026-11-01.json new file mode 100644 index 0000000..2b8061a --- /dev/null +++ b/docs/release/editor-attachments-fresh-recovery-2026-11-01.json @@ -0,0 +1,37 @@ +{ + "schema": "coulomb.image-release-independent-recovery.v1", + "status": "fresh_point_independently_retrieved_and_restored", + "completed_at": "2026-10-31T23:36:36.772911+00:00", + "captured_at": "2026-10-31T23:29:54Z", + "primary_version": "1793489481116168", + "mirror_version": "1793489725683064", + "mirror_receipt_version": "1793489727604085", + "ciphertext_sha256": "eb495239fdf2344d6d72da9d4b02957613e7a4d67c54a2d67963ef802bc39421", + "independent_reader_used": true, + "unpack": { + "status": "decrypted_pair_verified", + "version_id": "1793489481116168", + "encrypted_sha256": "eb495239fdf2344d6d72da9d4b02957613e7a4d67c54a2d67963ef802bc39421", + "captured_at": "2026-10-31T23:29:54Z", + "database_sha256": "4c49933e4fb81573257b7c64e95edd7f0395f20c9e6280348bb10383fd8eaa25", + "content_sha256": "0768e4f61ef003cff76c31aaca687e07e1743ff15a4d2d92dc0ebf123d4ec55e", + "application_restore_verified": false + }, + "restore": { + "status": "restored_verified", + "database_tables": 13, + "database_rows": 119, + "database_rows_and_sequences_match": true, + "content_files": 98, + "content_hashes_match": true, + "network_internal": true, + "published_ports": 0, + "postgres_image_id": "sha256:ff23cdce56cac62ada6f66013e1a50864c0abbe79d132d40b6e05bd80f378a70", + "elapsed_seconds": 20.38, + "content_was_empty": false + }, + "elapsed_seconds": 27.87, + "production_restore": false, + "current_kit_replaced": false, + "driver_sha256": "016bc4824ca885c765f8e7c267f1da8ad4b9c76ed47cc73c29c7168e6e2e1204" +} diff --git a/docs/release/editor-attachments-independent-postflight-2026-11-01.json b/docs/release/editor-attachments-independent-postflight-2026-11-01.json new file mode 100644 index 0000000..a9f0607 --- /dev/null +++ b/docs/release/editor-attachments-independent-postflight-2026-11-01.json @@ -0,0 +1,41 @@ +{ + "schema": "coulomb.content-acceptance-independent-postflight.v1", + "status": "deployed_image_and_complete_native_state_verified", + "proposal_digest": "sha256:2cddcf38b7052287c10dce467a200a74cfa9699c666ae9ac538fc82917129e7a", + "receipt_sha256": "abf5910f3b68790356cb2b579038401ef7395e8f213fd11b0f89603fbc4ada32", + "helm_revision": 21, + "image": "forgejo.coulomb.social/coulomb/coulomb-social@sha256:b58d858abd5b4acbd6b893c3f622f7a9fdb5e606164ab9866eb3cfaa04df1a0e", + "source": "31eaa581a65e454b6ef9979326c5bdfe10e13806", + "packaged_source_files_verified": 79, + "sole_ready_content_writer": true, + "all_frozen_capsule_files_verified": 21, + "installed_helper_hashes_verified": true, + "snapshot_pin_refreshed": true, + "public_health_landing_static_verified": true, + "postflight": { + "business_tables": 12, + "migration_rows_sha256": "43471516387c0e2a3d35971033ed6af330e2963efe4da93293d97633825d04af", + "business_rows": 97, + "business_rows_sha256": "35b0ae7d9dd1ff21c2ceaa04649162545ec36a2c68629548238352d9cd0fb303", + "content_files": 98, + "content_files_sha256": "b657c0e470399b6c7500da500cbbec061bea12ab9633bd74e2f142d797b2d26a", + "global_slug_constraint": true, + "tenant_slug_constraint": true, + "migration_0003_applied": true, + "duplicate_slug_groups": 0, + "actual_owner_pages_rendered": 43, + "unsaved_outsider_page_denials": 43, + "production_identity_or_grant_created": false + }, + "fresh_paired_point": { + "captured_at": "2026-10-31T23:29:54Z", + "destination": "s3://railiance-platform-pg-backup/platform-pg/application-archives/coulomb-social/2026/10/31/20261031T232954Z-039d1bab2ea7ef33b7453979.tar.age", + "version_id": "1793489481116168", + "encrypted_sha256": "eb495239fdf2344d6d72da9d4b02957613e7a4d67c54a2d67963ef802bc39421", + "encrypted_bytes": 2181848, + "image": "forgejo.coulomb.social/coulomb/coulomb-social@sha256:7906f461bc8d2d5204fd4fde67a251850f725b914452a66172919d6853672ed5" + }, + "timers_activated": false, + "production_data_restored": false, + "productive_acceptance_inferred": false +} diff --git a/docs/release/editor-attachments-live-editor-2026-11-01.json b/docs/release/editor-attachments-live-editor-2026-11-01.json new file mode 100644 index 0000000..e44859e --- /dev/null +++ b/docs/release/editor-attachments-live-editor-2026-11-01.json @@ -0,0 +1,25 @@ +{ + "schema": "coulomb.live-editor-attachment-check.v1", + "status": "live_saved_attachments_visible_in_editors", + "helm_revision": 21, + "actual_user_verdict_inferred": false, + "production_writes": false, + "checks": { + "business_tables": 12, + "migration_rows_sha256": "43471516387c0e2a3d35971033ed6af330e2963efe4da93293d97633825d04af", + "business_rows": 97, + "business_rows_sha256": "35b0ae7d9dd1ff21c2ceaa04649162545ec36a2c68629548238352d9cd0fb303", + "content_files": 98, + "content_files_sha256": "b657c0e470399b6c7500da500cbbec061bea12ab9633bd74e2f142d797b2d26a", + "global_slug_constraint": true, + "tenant_slug_constraint": true, + "migration_0003_applied": true, + "duplicate_slug_groups": 0, + "actual_owner_pages_rendered": 43, + "live_editors_rendered": 43, + "editors_with_saved_attachments": 41, + "editor_download_links_verified": 42, + "unsaved_outsider_page_denials": 43, + "production_identity_or_grant_created": false + } +} diff --git a/docs/release/editor-attachments-preflight-2026-10-31.json b/docs/release/editor-attachments-preflight-2026-10-31.json new file mode 100644 index 0000000..f009e3a --- /dev/null +++ b/docs/release/editor-attachments-preflight-2026-10-31.json @@ -0,0 +1,25 @@ +{ + "schema": "coulomb.guarded-image-release.v1", + "proposal_digest": "sha256:2cddcf38b7052287c10dce467a200a74cfa9699c666ae9ac538fc82917129e7a", + "started_at": "2026-10-31T21:15:08.583669+00:00", + "status": "live_release_preflight_verified", + "production_data_restored": false, + "timers_activated": false, + "node_request_percent": { + "cpu": 68, + "memory": 76 + }, + "preflight": { + "business_tables": 12, + "migration_rows_sha256": "43471516387c0e2a3d35971033ed6af330e2963efe4da93293d97633825d04af", + "business_rows": 97, + "business_rows_sha256": "35b0ae7d9dd1ff21c2ceaa04649162545ec36a2c68629548238352d9cd0fb303", + "content_files": 98, + "content_files_sha256": "b657c0e470399b6c7500da500cbbec061bea12ab9633bd74e2f142d797b2d26a", + "global_slug_constraint": true, + "tenant_slug_constraint": true, + "migration_0003_applied": true, + "duplicate_slug_groups": 0 + }, + "live_changes": false +} \ No newline at end of file diff --git a/docs/release/editor-attachments-provenance-2026-10-31.json b/docs/release/editor-attachments-provenance-2026-10-31.json new file mode 100644 index 0000000..df09c4b --- /dev/null +++ b/docs/release/editor-attachments-provenance-2026-10-31.json @@ -0,0 +1,11 @@ +{ + "schema": "coulomb.packaged-source-provenance.v1", + "image": "forgejo.coulomb.social/coulomb/coulomb-social@sha256:b58d858abd5b4acbd6b893c3f622f7a9fdb5e606164ab9866eb3cfaa04df1a0e", + "source": "31eaa581a65e454b6ef9979326c5bdfe10e13806", + "packaged_file_count": 79, + "all_packaged_source_bytes_match": true, + "source_revision_label_verified": true, + "registry_digest_resolves": true, + "application_source_overlay": false, + "manifest_sha256": "99401a60fdd9a95aff1168858e81fd7d8802cd7603f15c0ea35eb6917981f9fe" +} diff --git a/docs/release/editor-attachments-receipt-2026-11-01.json b/docs/release/editor-attachments-receipt-2026-11-01.json new file mode 100644 index 0000000..54ec864 --- /dev/null +++ b/docs/release/editor-attachments-receipt-2026-11-01.json @@ -0,0 +1,70 @@ +{ + "completed_at": "2026-10-31T23:32:38.446589+00:00", + "elapsed_seconds": 173.091, + "existing_global_constraint_preserved": true, + "fresh_paired_point": { + "age_recipient": "age1qm30jk8q0cezey652dk2dhcr3d9vs528v6lg8mavl6d4q3e3vgnqmrmyy3", + "application": "coulomb-social", + "application_restore_verified": false, + "captured_at": "2026-10-31T23:29:54Z", + "ciphertext_bytes": 2181848, + "ciphertext_sha256": "eb495239fdf2344d6d72da9d4b02957613e7a4d67c54a2d67963ef802bc39421", + "completed_at": "2026-10-31T23:31:21.582167+00:00", + "content_sha256": "0768e4f61ef003cff76c31aaca687e07e1743ff15a4d2d92dc0ebf123d4ec55e", + "database_sha256": "4c49933e4fb81573257b7c64e95edd7f0395f20c9e6280348bb10383fd8eaa25", + "destination": "s3://railiance-platform-pg-backup/platform-pg/application-archives/coulomb-social/2026/10/31/20261031T232954Z-039d1bab2ea7ef33b7453979.tar.age", + "encrypted_bytes": 2181848, + "encrypted_sha256": "eb495239fdf2344d6d72da9d4b02957613e7a4d67c54a2d67963ef802bc39421", + "image": "forgejo.coulomb.social/coulomb/coulomb-social@sha256:7906f461bc8d2d5204fd4fde67a251850f725b914452a66172919d6853672ed5", + "immutable_create": true, + "local_archive": "20261031T232954Z-039d1bab2ea7ef33b7453979.tar.age", + "object_key": "platform-pg/application-archives/coulomb-social/2026/10/31/20261031T232954Z-039d1bab2ea7ef33b7453979.tar.age", + "pause_and_capture_seconds": 85.7, + "remote_retrieval_verified": true, + "schema": "platform.application-primary-archive.v1", + "status": "offsite_fetched_verified", + "version_id": "1793489481116168" + }, + "helm_revision": 21, + "node_request_percent": { + "cpu": 68, + "memory": 76 + }, + "pid": 2007089, + "postflight": { + "actual_owner_pages_rendered": 43, + "business_rows": 97, + "business_rows_sha256": "35b0ae7d9dd1ff21c2ceaa04649162545ec36a2c68629548238352d9cd0fb303", + "business_tables": 12, + "content_files": 98, + "content_files_sha256": "b657c0e470399b6c7500da500cbbec061bea12ab9633bd74e2f142d797b2d26a", + "duplicate_slug_groups": 0, + "global_slug_constraint": true, + "migration_0003_applied": true, + "migration_rows_sha256": "43471516387c0e2a3d35971033ed6af330e2963efe4da93293d97633825d04af", + "production_identity_or_grant_created": false, + "tenant_slug_constraint": true, + "unsaved_outsider_page_denials": 43 + }, + "preflight": { + "business_rows": 97, + "business_rows_sha256": "35b0ae7d9dd1ff21c2ceaa04649162545ec36a2c68629548238352d9cd0fb303", + "business_tables": 12, + "content_files": 98, + "content_files_sha256": "b657c0e470399b6c7500da500cbbec061bea12ab9633bd74e2f142d797b2d26a", + "duplicate_slug_groups": 0, + "global_slug_constraint": true, + "migration_0003_applied": true, + "migration_rows_sha256": "43471516387c0e2a3d35971033ed6af330e2963efe4da93293d97633825d04af", + "tenant_slug_constraint": true + }, + "private_values_printed": false, + "production_data_restored": false, + "proposal_digest": "sha256:2cddcf38b7052287c10dce467a200a74cfa9699c666ae9ac538fc82917129e7a", + "public_health_landing_static_verified": true, + "schema": "coulomb.guarded-image-release.v1", + "snapshot_pin_refreshed": true, + "started_at": "2026-10-31T23:29:45.196471+00:00", + "status": "image_candidate_deployed_verified", + "timers_activated": false +} \ No newline at end of file diff --git a/docs/release/editor-attachments-reconciliation-2026-11-01.json b/docs/release/editor-attachments-reconciliation-2026-11-01.json new file mode 100644 index 0000000..58059e2 --- /dev/null +++ b/docs/release/editor-attachments-reconciliation-2026-11-01.json @@ -0,0 +1,17 @@ +{ + "schema": "coulomb.editor-release-reconciliation.v1", + "helm_revision": 21, + "snapshot_server_dry_run": { + "status": "reviewed", + "server_dry_run": true, + "image_pinned": true, + "pvc_read_only": true, + "db_scope": "coulomb_social_db", + "replicas_restored_to": 1 + }, + "backup_timers_inactive_disabled": true, + "snapshot_source_sha256": "d680edba707394bc1f5857fa86676d21e6a82d1e1b3b7838dfb628938de7529a", + "owner_live_values_match_sealed_values": true, + "snapshot_restore_regression_tests_passed": 9, + "recovery_container_stopped": true +} diff --git a/docs/release/editor-attachments-recovery-2026-10-31.json b/docs/release/editor-attachments-recovery-2026-10-31.json new file mode 100644 index 0000000..1623bcb --- /dev/null +++ b/docs/release/editor-attachments-recovery-2026-10-31.json @@ -0,0 +1,67 @@ +{ + "schema": "coulomb.packaged-content-acceptance-recovery.v1", + "status": "packaged_recovered_export_and_views_verified", + "source": "31eaa581a65e454b6ef9979326c5bdfe10e13806", + "image": "forgejo.coulomb.social/coulomb/coulomb-social@sha256:b58d858abd5b4acbd6b893c3f622f7a9fdb5e606164ab9866eb3cfaa04df1a0e", + "restore_receipt_sha256": "9dacbd44544f93abf99354158cf610b4fb1f4e59611b22056b3086e0033231ab", + "retained_point_captured_at": "2026-10-31T20:21:01Z", + "candidate_application": { + "business_tables": 12, + "migration_rows_sha256": "43471516387c0e2a3d35971033ed6af330e2963efe4da93293d97633825d04af", + "business_rows": 93, + "business_rows_sha256": "7953b37b02b80d89ca6869dab6b2b0b996541de20ca0dd1b294a5f25572fb2ca", + "content_files": 86, + "content_files_sha256": "ed535246f4e8f46fcd7ce01f936127996aff873c6936b297f7272194b594aeda", + "global_slug_constraint": true, + "tenant_slug_constraint": true, + "migration_0003_applied": true, + "duplicate_slug_groups": 0, + "actual_owner_pages_rendered": 40, + "unsaved_outsider_page_denials": 40, + "production_identity_or_grant_created": false + }, + "previous_image_application": { + "business_tables": 12, + "migration_rows_sha256": "43471516387c0e2a3d35971033ed6af330e2963efe4da93293d97633825d04af", + "business_rows": 93, + "business_rows_sha256": "7953b37b02b80d89ca6869dab6b2b0b996541de20ca0dd1b294a5f25572fb2ca", + "content_files": 86, + "content_files_sha256": "ed535246f4e8f46fcd7ce01f936127996aff873c6936b297f7272194b594aeda", + "global_slug_constraint": true, + "tenant_slug_constraint": true, + "migration_0003_applied": true, + "duplicate_slug_groups": 0, + "actual_owner_pages_rendered": 40, + "unsaved_outsider_page_denials": 40, + "production_identity_or_grant_created": false + }, + "client_export": { + "status": "recovered_client_export_verified", + "schema": "coulomb.client-dataset-export.v1", + "content_files": 85, + "records": { + "members.user": 1, + "members.member": 1, + "spaces.space": 1, + "spaces.spacemembership": 1 + }, + "admin_events": 0, + "archive_sha256": "ecf5dba7ce891475cd5df15eb30205606f805850af7339ecd46e1479b45d6c5f", + "offboarding_complete": false, + "all_source_tables_compared": 13, + "all_source_files_compared": 86, + "archive_member_hashes_verified": true, + "selected_client_content_complete_locally": true, + "source_sql_and_content_unchanged": true, + "external_bindings_require_review": 0, + "sessions_and_credentials_excluded": true + }, + "source_content_files_unchanged": 86, + "all_tables_unchanged": true, + "application_source_overlay": false, + "network_internal": true, + "published_ports": 0, + "production_access": false, + "production_restore": false, + "driver_sha256": "29c53d551eec2b4f4d60b047f123c727eef76ee7677c054bbfb0ab008355cff9" +} diff --git a/docs/release/editor-attachments-return-2026-11-01.sources.txt b/docs/release/editor-attachments-return-2026-11-01.sources.txt new file mode 100644 index 0000000..5492d84 --- /dev/null +++ b/docs/release/editor-attachments-return-2026-11-01.sources.txt @@ -0,0 +1,4 @@ +/home/worsch/.config/railiance-platform/coulomb-social-recovery/editor-point-recovery-20261101/csoc-mirror-editor-point.py: 2cba828db15a11415136eb17586c116ba10d06b835b5f8291ae858af7ad2b448 +/home/worsch/.config/railiance-platform/coulomb-social-recovery/editor-point-recovery-20261101/csoc-recover-editor-point.py: 016bc4824ca885c765f8e7c267f1da8ad4b9c76ed47cc73c29c7168e6e2e1204 +/home/worsch/.config/railiance-platform/coulomb-social-recovery/editor-point-recovery-20261101/csoc-verify-editor-fresh-point.py: 5bfac786cbbf64bdbbd5b4b19d9fddfe0f99a28499417dbbbc0c2ca74e2cc69a +/home/worsch/.config/railiance-platform/coulomb-social-recovery/editor-point-recovery-20261101/csoc-live-editor-check.py: 679336ef9d952e24f5138532cb9cdba84e5635c9a35a54ee7c80312e249d73b7 diff --git a/docs/release/editor-attachments-target-2026-10-31.json b/docs/release/editor-attachments-target-2026-10-31.json new file mode 100644 index 0000000..023ddde --- /dev/null +++ b/docs/release/editor-attachments-target-2026-10-31.json @@ -0,0 +1,21 @@ +{ + "baseline_revision": 20, + "baseline_helm_manifest_sha256": "e388f62c97e97d54d87671825bd0b05c854a70b0cca8a00faee452ae7841799e", + "deployment_uid": "2dbe661a-d232-4477-bbd1-833813077fbe", + "baseline_spec_sha256": "8d79c64cff4bd2c62f03494e80847dc99b965ddabf35645da67e8d1020220798", + "candidate_spec_sha256": "4deab26fc15d63e09922259b3362cd4e5c6bfa10884de058f776c0de8b642b00", + "old_image": "forgejo.coulomb.social/coulomb/coulomb-social@sha256:7906f461bc8d2d5204fd4fde67a251850f725b914452a66172919d6853672ed5", + "expected_data": { + "business_tables": 12, + "migration_rows_sha256": "43471516387c0e2a3d35971033ed6af330e2963efe4da93293d97633825d04af", + "business_rows": 97, + "business_rows_sha256": "35b0ae7d9dd1ff21c2ceaa04649162545ec36a2c68629548238352d9cd0fb303", + "content_files": 98, + "content_files_sha256": "b657c0e470399b6c7500da500cbbec061bea12ab9633bd74e2f142d797b2d26a", + "global_slug_constraint": true, + "tenant_slug_constraint": true, + "migration_0003_applied": true, + "duplicate_slug_groups": 0 + }, + "image_only_server_preview": true +} diff --git a/docs/release/editor-attachments-tests-2026-10-31.json b/docs/release/editor-attachments-tests-2026-10-31.json new file mode 100644 index 0000000..3570551 --- /dev/null +++ b/docs/release/editor-attachments-tests-2026-10-31.json @@ -0,0 +1,13 @@ +{ + "schema": "coulomb.packaged-content-acceptance-tests.v1", + "image": "forgejo.coulomb.social/coulomb/coulomb-social@sha256:b58d858abd5b4acbd6b893c3f622f7a9fdb5e606164ab9866eb3cfaa04df1a0e", + "source": "31eaa581a65e454b6ef9979326c5bdfe10e13806", + "tests": 110, + "failures": 0, + "errors": 0, + "skipped": 0, + "network": "none", + "read_only_root": true, + "application_source_overlay": false, + "synthetic_fixtures_only": true +} diff --git a/docs/release/editor-attachments-user-return-2026-11-01.json b/docs/release/editor-attachments-user-return-2026-11-01.json new file mode 100644 index 0000000..36f1192 --- /dev/null +++ b/docs/release/editor-attachments-user-return-2026-11-01.json @@ -0,0 +1,11 @@ +{ + "schema": "coulomb.productive-editor-attachment-return.v1", + "helm_revision": 21, + "source": "31eaa581a65e454b6ef9979326c5bdfe10e13806", + "user": "Bernd", + "statement": "Both tabs now show the attachment", + "status": "reported_editor_visibility_gap_accepted_resolved", + "scope": "Attachment visibility in both reopened editor tabs", + "copy_move_sharing_portability_acceptance_inferred": false, + "complete_attachment_workflow_acceptance_inferred": false +} diff --git a/docs/release/editor-attachments-validation-2026-10-31.json b/docs/release/editor-attachments-validation-2026-10-31.json new file mode 100644 index 0000000..23e9b42 --- /dev/null +++ b/docs/release/editor-attachments-validation-2026-10-31.json @@ -0,0 +1,34 @@ +{ + "schema": "coulomb.editor-attachments-validation.v1", + "reported_gap": "Attachments visible on the original page were absent from editor tabs.", + "main_source": "8b38afddf3f3648f209c60b1bafe3dd25501d857", + "release_source": "31eaa581a65e454b6ef9979326c5bdfe10e13806", + "image": "forgejo.coulomb.social/coulomb/coulomb-social@sha256:b58d858abd5b4acbd6b893c3f622f7a9fdb5e606164ab9866eb3cfaa04df1a0e", + "main_full_gate": { + "tests": 670, + "failures": 0, + "errors": 0, + "skipped": 0 + }, + "release_full_gate": { + "tests": 568, + "failures": 0, + "errors": 0, + "skipped": 0 + }, + "packaged_tests": 110, + "browser_verifies": [ + "saved file list and exact download bytes in both editor tabs", + "management opens separately and keeps unsaved text", + "stale refusal keeps submitted text and attachments", + "save keeps attachments and rejects stale overwrite", + "existing copy/move/download/detach permissions journey" + ], + "runtime_changes": [ + "coulomb_social/apps/spaces/views.py", + "coulomb_social/templates/spaces/page_form.html" + ], + "storage_schema_or_authority_changed": false, + "productive_acceptance_inferred": false, + "test_environment": "Main uses /var/tmp; isolated branch native observer requires /tmp. Final complete gates pass without deselection." +} diff --git a/docs/release/existing-telemetry-2026-10-31.json b/docs/release/existing-telemetry-2026-10-31.json new file mode 100644 index 0000000..f7ab41e --- /dev/null +++ b/docs/release/existing-telemetry-2026-10-31.json @@ -0,0 +1,200 @@ +{ + "schema": "coulomb.service-measurement-gap.v1", + "recorded_at": "2026-10-31T14:43:33.505165+00:00", + "task": "CSOC-WP-0012-T11", + "current_signals": { + "schema": "coulomb.existing-telemetry-observation.v1", + "observed_at": "2026-10-31T14:39:40.395913+00:00", + "service_name": "telemetry-prometheus", + "service_uid": "529df0e7-b8a6-4478-8f51-672f50e9eaae", + "selected_metric_names": [], + "queries": { + "coulomb_ready": "kube_deployment_status_replicas_available{namespace=\"coulomb-social\"}", + "coulomb_ingress_metadata": "kube_ingress_info{namespace=\"coulomb-social\"}", + "coulomb_scrape_targets": "up{namespace=\"coulomb-social\"}", + "probe_series_count": "count(probe_success)", + "traefik_series_count": "count(traefik_service_requests_total)" + }, + "results": { + "coulomb_ready": { + "resultType": "vector", + "result": [ + { + "metric": { + "__name__": "kube_deployment_status_replicas_available", + "container": "kube-state-metrics", + "deployment": "coulomb-social", + "endpoint": "http", + "instance": "10.42.0.7:8080", + "job": "kube-state-metrics", + "namespace": "coulomb-social", + "pod": "telemetry-kube-state-metrics-5c9cd96d7b-c7ch5", + "service": "telemetry-kube-state-metrics" + }, + "value": [ + 1793457579.499, + "1" + ] + } + ] + }, + "coulomb_ingress_metadata": { + "resultType": "vector", + "result": [ + { + "metric": { + "__name__": "kube_ingress_info", + "container": "kube-state-metrics", + "endpoint": "http", + "ingress": "coulomb-social", + "ingressclass": "traefik", + "instance": "10.42.0.7:8080", + "job": "kube-state-metrics", + "namespace": "coulomb-social", + "pod": "telemetry-kube-state-metrics-5c9cd96d7b-c7ch5", + "service": "telemetry-kube-state-metrics" + }, + "value": [ + 1793457579.742, + "1" + ] + } + ] + }, + "coulomb_scrape_targets": { + "resultType": "vector", + "result": [] + }, + "probe_series_count": { + "resultType": "vector", + "result": [] + }, + "traefik_series_count": { + "resultType": "vector", + "result": [] + } + }, + "runtime_modified": false + }, + "retention_and_readiness_window": { + "observed_at": "2026-10-31T14:41:38.172491+00:00", + "retention_flags": { + "storage.tsdb.retention.size": "10GiB", + "storage.tsdb.retention.time": "10d" + }, + "queries": { + "ready_samples_7d": "sum(count_over_time(kube_deployment_status_replicas_available{namespace=\"coulomb-social\",deployment=\"coulomb-social\"}[7d]))", + "minimum_available_replicas_7d": "min(min_over_time(kube_deployment_status_replicas_available{namespace=\"coulomb-social\",deployment=\"coulomb-social\"}[7d]))", + "maximum_available_replicas_7d": "max(max_over_time(kube_deployment_status_replicas_available{namespace=\"coulomb-social\",deployment=\"coulomb-social\"}[7d]))" + }, + "results": { + "ready_samples_7d": { + "resultType": "vector", + "result": [ + { + "metric": {}, + "value": [ + 1793457697.673, + "20160" + ] + } + ] + }, + "minimum_available_replicas_7d": { + "resultType": "vector", + "result": [ + { + "metric": {}, + "value": [ + 1793457697.955, + "0" + ] + } + ] + }, + "maximum_available_replicas_7d": { + "resultType": "vector", + "result": [ + { + "metric": {}, + "value": [ + 1793457698.156, + "1" + ] + } + ] + } + }, + "end_to_end_slo_proven": false, + "runtime_modified": false + }, + "pending_execution_receipts": { + "observed_at": "2026-10-31T14:38:31.143707+00:00", + "receipts": { + "custody": { + "exists": false + }, + "route": { + "exists": false + }, + "installation": { + "exists": false + }, + "content_release": { + "exists": false + } + }, + "mutations": false + }, + "interpretation": { + "application_slo_attainment_proven": false, + "monthly_application_window_proven": false, + "readiness_equals_http_availability": false, + "sample_count_proves_gapless_coverage": false, + "maintenance_or_dependency_exclusions_applied": false, + "application_latency_percentiles_available": false, + "configuration_changed": false, + "requires_application_level_signals_and_window_evidence": true + }, + "sources": [ + { + "path": "registry/release-units/coulomb-social.yaml", + "sha256": "309102bdee9d0ec0057c5d679be729ebd7aa3477d588962c2e757a38946761f6" + }, + { + "path": "docs/resource-evidence/service-objectives-and-observations-2026-08.md", + "sha256": "f989738e569022cab572b676a64bc58565b2fad9b9476ea0a0d3c799e7c8d4e4" + }, + { + "path": "docs/release/identity-export-release-return-2026-10-31.md", + "sha256": "e78222c650ff3939bb8e8f550e9b75534c9f43badea612a4ea3a2792fba82d01" + } + ], + "current_release": { + "observed_at": "2026-10-31T14:44:54.910350+00:00", + "deployment_uid": "2dbe661a-d232-4477-bbd1-833813077fbe", + "generation": 49, + "observed_generation": 49, + "ready_replicas": 1, + "images": [ + "forgejo.coulomb.social/coulomb/coulomb-social@sha256:522bcd7fce92436765a60c1e4afa2598f3cc40913352153ff1c4319a99ca92ed" + ], + "helm_revision": 19, + "helm_status": "deployed" + }, + "statehub_readback": { + "task_id": "61f5f196-40e4-5901-a10e-318304809a00", + "status": "progress", + "measurement_gap_synced": true, + "fix_consistency_exit_code": 0, + "automation_errors": 0, + "assessment_failures": 0, + "warnings": 6 + }, + "validation": { + "native_read_results_checked": true, + "json_round_trip_checked": true, + "application_code_changed": false, + "new_regression_run_required": false + } +} diff --git a/docs/release/goal-progress-audit-2026-10-31.md b/docs/release/goal-progress-audit-2026-10-31.md index a0592fa..65e716a 100644 --- a/docs/release/goal-progress-audit-2026-10-31.md +++ b/docs/release/goal-progress-audit-2026-10-31.md @@ -139,3 +139,59 @@ is complete and waits on T04 historical custody/capacity and T07. T04 remains in progress for those native returns and historical-copy disposition. The original roughly 40% remaining estimate is unchanged: these source fixes do not establish recurring operation, productive acceptance or full remaining-cohort transfer. + + +## Service provenance checkpoint + +WP-0012-T14 now closes the Forgejo successor and existing founder identity-choice +bindings formerly inside T11. The records include resolved decisions, completed +owner migration and fresh read-only native metadata. T11 advances to progress and +retains actual ops registration, client/app/instance finance attribution/report, +current SLO and stage-1 smoke. This makes the source checklist13of32done; it is an +additional independently verified requirement, not a new product scope or an +updated effort estimate. The rough remaining-work estimate is unchanged. + + +## Visual preservation checkpoint + +WP-0014-T08 completes the previously missing private Visual capture and draft-tree +materialization:242references,108URLs,90distinct images,236article pages and67 +space indexes. Offline decoding and independent body/date/source preservation +checks pass. This advances the source checklist to14of33done, not an effort +percentage. Source exceptions, identity/grant mapping, links/attachments and +productive transfer/verdicts remain open; the rough remaining-effort estimate +has not been recalculated from these counts. + +## Access reconciliation checkpoint + +WP-0014-T01 now has a private, source-hash-bound access inventory covering the +70 fresh spaces and 245 articles, while retaining the inventory-only 71st space. +It identifies three distinct source principals and nine unresolved creator +references, preserving missing access fields as unknown. Existing ReichelAG +admission does not establish all-space identity or role mappings. The task remains +progress: this closes an evidence gap inside the task, not productive permission +reconciliation. The checklist stays 14 of 33 done, and the remaining-effort +estimate is unchanged. Evidence: `bubble-access-audit-2026-10-31.json`. + +## Offboarding scope checkpoint + +WP-0012-T12 moves from todo to progress with a concrete non-destructive scope +review. Fresh native evidence distinguishes the accepted85file tenant export from +the second, externally bound space; confirms the Helm-retained content PVC; and +accounts for five still-running isolated recovery databases and their receipts. +The review includes sessions, root journals, private copies and protected backup +holds. No teardown, deletion, revocation or expiry occurred. Canonical client +scope, agreed retention, procedure proof and private delivery/acceptance remain +open. The checklist stays14of33done; the effort estimate is unchanged. Evidence: +`client-offboarding-scope-2026-10-31.json` and its human-readable scope review. + +## Corpus restart checkpoint + +WP-0014-T07 moves from todo to progress with opt-in durable per-space import +receipts. Completed boundaries can be resumed without replaying completed spaces; +uncertain in-space outcomes require reconciliation. Synthetic process-exit tests +exercise both conditions. No remaining production cohort was imported. Source and +identity/grant reconciliation, supervised executor integration and productive +postflight/verdicts remain open. The checklist stays14of33done and the rough +remaining-effort estimate is unchanged. Evidence: +`corpus-import-receipts-2026-10-31.md` and `tests/test_import_receipts.py`. diff --git a/docs/release/mirror-content-point-native-2026-10-31.json b/docs/release/mirror-content-point-native-2026-10-31.json new file mode 100644 index 0000000..4bdf2b6 --- /dev/null +++ b/docs/release/mirror-content-point-native-2026-10-31.json @@ -0,0 +1,72 @@ +{ + "schema": "coulomb.native-mirror-worker-proof.v1", + "status": "native_worker_and_independent_receipt_verified", + "completed_at": "2026-10-31T20:27:08.869833+00:00", + "primary": { + "application": "coulomb-social", + "captured_at": "2026-10-31T20:21:01Z", + "object_key": "platform-pg/application-archives/coulomb-social/2026/10/31/20261031T202101Z-2ad4faf215939d67e82f67a4.tar.age", + "version_id": "1793478121590106", + "ciphertext_sha256": "91b4ffe4874a53e3422d604b7c3fa85f6551a481411ef82b2209993552486bfb", + "ciphertext_bytes": 2181848, + "age_recipient": "age1qm30jk8q0cezey652dk2dhcr3d9vs528v6lg8mavl6d4q3e3vgnqmrmyy3", + "image": "forgejo.coulomb.social/coulomb/coulomb-social@sha256:522bcd7fce92436765a60c1e4afa2598f3cc40913352153ff1c4319a99ca92ed", + "database_sha256": "c5faa20ac0c443635393e04cf5b154710780d829d6c085edc098e2dd7525fc6a", + "content_sha256": "1bf31bafc19d8c4847f3793463189f13ce4b470ed96ccdc614b269acca6c12c0" + }, + "recovery_mirror": { + "bucket": "railiance-coulomb-social-archives", + "ciphertext_bytes": 2181848, + "ciphertext_sha256": "91b4ffe4874a53e3422d604b7c3fa85f6551a481411ef82b2209993552486bfb", + "hash_readback_verified": true, + "object_key": "archives/platform-pg/application-archives/coulomb-social/2026/10/31/20261031T202101Z-2ad4faf215939d67e82f67a4.tar.age", + "project_id": "06d107bc-9d84-4672-b2a0-95701cc0c07f", + "receipt_bytes": 1664, + "receipt_object_key": "archives/platform-pg/application-archives/coulomb-social/2026/10/31/20261031T202101Z-2ad4faf215939d67e82f67a4.tar.age.741bdf7b02b859523053f45c88c874b58d43b45a0273bb315116d2bfc34e58aa.receipt.json", + "receipt_readback_verified": true, + "receipt_sha256": "741bdf7b02b859523053f45c88c874b58d43b45a0273bb315116d2bfc34e58aa", + "receipt_version_id": "1793478427159655", + "version_id": "1793478426025763" + }, + "worker_results": [ + { + "status": "mirrored_with_retained_receipt", + "primary_recaptured": false, + "ciphertext_readback_verified": true, + "receipt_readback_verified": true, + "recovery_key_read": false, + "independent_reader_used": false, + "retained_objects_deleted": false, + "application_restore_verified": false + }, + { + "status": "mirrored_with_retained_receipt", + "primary_recaptured": false, + "ciphertext_readback_verified": true, + "receipt_readback_verified": true, + "recovery_key_read": false, + "independent_reader_used": false, + "retained_objects_deleted": false, + "application_restore_verified": false + } + ], + "native_versions_before": 0, + "native_versions_after": 2, + "one_ciphertext_and_one_receipt_version_after_retry": true, + "independent_reader_ciphertext_and_receipt_verified": true, + "retained_receipt_equals_exact_public_field_filter": true, + "local_primary_receipt_and_ciphertext_unchanged": true, + "retry_last_mirror_bytes_unchanged": true, + "runtime_source_commit": "09f4fd33cbeefecb833998246cf53848bb1e5d8d", + "private_state_copy_used": true, + "production_application_contact": false, + "node_helpers_or_state_modified": false, + "node_timers_changed": false, + "native_capture_performed": false, + "decryption_identity_read": false, + "retained_objects_deleted": false, + "operating_capacity_profile_admitted": false, + "alert_reception_verified": false, + "scheduled_service_execution_verified": false, + "sources_manifest_sha256": "a865efe1cbf468c34e8f06912a6d423845ce08fc8a9963936f1e8af73897cad2" +} diff --git a/docs/release/mirror-content-point-native-2026-10-31.sources.txt b/docs/release/mirror-content-point-native-2026-10-31.sources.txt new file mode 100644 index 0000000..de3adb6 --- /dev/null +++ b/docs/release/mirror-content-point-native-2026-10-31.sources.txt @@ -0,0 +1,9 @@ +/home/worsch/coulomb-social/deploy/backup/mirror-platform-runtime.json: 9253c5f6e956b67f499d3e12d2851be7fabd3bc56b00496b8810bbd0be8209f1 +/home/worsch/coulomb-social/scripts/backup_capacity.py: cb699a79e67b129239b5aa5d6f3b0d4ffaa9773a8b073227ad17fdecb5b9d893 +/home/worsch/coulomb-social/scripts/check-paired-backup-health.py: 2d40ec6e4c59ea7fe1d46065d197b9a87ca6811f023cc6f3e20e4184a3490566 +/home/worsch/coulomb-social/scripts/mirror-paired-backup.py: 9ceb26d9083af016c4b787907e4dce7bdf437c782fb2d15eb28d9e334e6e431c +/home/worsch/railiance-platform/scripts/application_recovery_identities.py: 20f56255ed6eeec4aafe33f142bc3ee03f623d4957a7b0cabb741d340f15f79d +/home/worsch/railiance-platform/scripts/application_recovery_mirror.py: d48ab95e89b9eeae36269d853ebe4c44c22e1f57d4e14224f03d0b9aeeb34178 +/home/worsch/railiance-platform/scripts/application_recovery_resources.py: ab70ec341e738272730eb0daf76840d0bca807caeb3c4e4c2357f162fbed4498 +/home/worsch/railiance-platform/scripts/primary_application_archive.py: a3f0a61e869a6012edb4b5288be14e3b0c4a9a1273b090320ab146d12e5aa94d +/tmp/csoc-mirror-content-point.py: bc4d04c3f8ee3ca089fa9b038e6ab617eaababcb7f0aec94339e7f4eeb70f13a diff --git a/docs/release/mirror-editor-point-native-2026-11-01.json b/docs/release/mirror-editor-point-native-2026-11-01.json new file mode 100644 index 0000000..f987fd4 --- /dev/null +++ b/docs/release/mirror-editor-point-native-2026-11-01.json @@ -0,0 +1,72 @@ +{ + "schema": "coulomb.native-mirror-worker-proof.v1", + "status": "native_worker_and_independent_receipt_verified", + "completed_at": "2026-10-31T23:35:37.716717+00:00", + "primary": { + "application": "coulomb-social", + "captured_at": "2026-10-31T23:29:54Z", + "object_key": "platform-pg/application-archives/coulomb-social/2026/10/31/20261031T232954Z-039d1bab2ea7ef33b7453979.tar.age", + "version_id": "1793489481116168", + "ciphertext_sha256": "eb495239fdf2344d6d72da9d4b02957613e7a4d67c54a2d67963ef802bc39421", + "ciphertext_bytes": 2181848, + "age_recipient": "age1qm30jk8q0cezey652dk2dhcr3d9vs528v6lg8mavl6d4q3e3vgnqmrmyy3", + "image": "forgejo.coulomb.social/coulomb/coulomb-social@sha256:7906f461bc8d2d5204fd4fde67a251850f725b914452a66172919d6853672ed5", + "database_sha256": "4c49933e4fb81573257b7c64e95edd7f0395f20c9e6280348bb10383fd8eaa25", + "content_sha256": "0768e4f61ef003cff76c31aaca687e07e1743ff15a4d2d92dc0ebf123d4ec55e" + }, + "recovery_mirror": { + "bucket": "railiance-coulomb-social-archives", + "ciphertext_bytes": 2181848, + "ciphertext_sha256": "eb495239fdf2344d6d72da9d4b02957613e7a4d67c54a2d67963ef802bc39421", + "hash_readback_verified": true, + "object_key": "archives/platform-pg/application-archives/coulomb-social/2026/10/31/20261031T232954Z-039d1bab2ea7ef33b7453979.tar.age", + "project_id": "06d107bc-9d84-4672-b2a0-95701cc0c07f", + "receipt_bytes": 1664, + "receipt_object_key": "archives/platform-pg/application-archives/coulomb-social/2026/10/31/20261031T232954Z-039d1bab2ea7ef33b7453979.tar.age.1d1589e5e6dc5edc6354434bf851ffe07c65c92eb7fcf1ff296aea7bca67c91b.receipt.json", + "receipt_readback_verified": true, + "receipt_sha256": "1d1589e5e6dc5edc6354434bf851ffe07c65c92eb7fcf1ff296aea7bca67c91b", + "receipt_version_id": "1793489727604085", + "version_id": "1793489725683064" + }, + "worker_results": [ + { + "status": "mirrored_with_retained_receipt", + "primary_recaptured": false, + "ciphertext_readback_verified": true, + "receipt_readback_verified": true, + "recovery_key_read": false, + "independent_reader_used": false, + "retained_objects_deleted": false, + "application_restore_verified": false + }, + { + "status": "mirrored_with_retained_receipt", + "primary_recaptured": false, + "ciphertext_readback_verified": true, + "receipt_readback_verified": true, + "recovery_key_read": false, + "independent_reader_used": false, + "retained_objects_deleted": false, + "application_restore_verified": false + } + ], + "native_versions_before": 0, + "native_versions_after": 2, + "one_ciphertext_and_one_receipt_version_after_retry": true, + "independent_reader_ciphertext_and_receipt_verified": true, + "retained_receipt_equals_exact_public_field_filter": true, + "local_primary_receipt_and_ciphertext_unchanged": true, + "retry_last_mirror_bytes_unchanged": true, + "runtime_source_commit": "09f4fd33cbeefecb833998246cf53848bb1e5d8d", + "private_state_copy_used": true, + "production_application_contact": false, + "node_helpers_or_state_modified": false, + "node_timers_changed": false, + "native_capture_performed": false, + "decryption_identity_read": false, + "retained_objects_deleted": false, + "operating_capacity_profile_admitted": false, + "alert_reception_verified": false, + "scheduled_service_execution_verified": false, + "sources_manifest_sha256": "dc4648e896c65ba0efccb80ff3b545b8efd582e1c7f54b6152ee0194b5af9eda" +} diff --git a/docs/release/mirror-editor-point-native-2026-11-01.sources.txt b/docs/release/mirror-editor-point-native-2026-11-01.sources.txt new file mode 100644 index 0000000..4ae231d --- /dev/null +++ b/docs/release/mirror-editor-point-native-2026-11-01.sources.txt @@ -0,0 +1,9 @@ +/home/worsch/coulomb-social/deploy/backup/mirror-platform-runtime.json: 9253c5f6e956b67f499d3e12d2851be7fabd3bc56b00496b8810bbd0be8209f1 +/home/worsch/coulomb-social/scripts/backup_capacity.py: cb699a79e67b129239b5aa5d6f3b0d4ffaa9773a8b073227ad17fdecb5b9d893 +/home/worsch/coulomb-social/scripts/check-paired-backup-health.py: 2d40ec6e4c59ea7fe1d46065d197b9a87ca6811f023cc6f3e20e4184a3490566 +/home/worsch/coulomb-social/scripts/mirror-paired-backup.py: 9ceb26d9083af016c4b787907e4dce7bdf437c782fb2d15eb28d9e334e6e431c +/home/worsch/railiance-platform/scripts/application_recovery_identities.py: 20f56255ed6eeec4aafe33f142bc3ee03f623d4957a7b0cabb741d340f15f79d +/home/worsch/railiance-platform/scripts/application_recovery_mirror.py: d48ab95e89b9eeae36269d853ebe4c44c22e1f57d4e14224f03d0b9aeeb34178 +/home/worsch/railiance-platform/scripts/application_recovery_resources.py: ab70ec341e738272730eb0daf76840d0bca807caeb3c4e4c2357f162fbed4498 +/home/worsch/railiance-platform/scripts/primary_application_archive.py: a3f0a61e869a6012edb4b5288be14e3b0c4a9a1273b090320ab146d12e5aa94d +/tmp/csoc-mirror-editor-point.py: 2cba828db15a11415136eb17586c116ba10d06b835b5f8291ae858af7ad2b448 diff --git a/docs/release/native-password-reset-review-2026-10-30.md b/docs/release/native-password-reset-review-2026-10-30.md index f1a7c51..716d0b7 100644 --- a/docs/release/native-password-reset-review-2026-10-30.md +++ b/docs/release/native-password-reset-review-2026-10-30.md @@ -1,6 +1,6 @@ # Native password-reset bind prerequisite -WP-0015-T04 remains open. An isolated native Authelia/LLDAP test identifies a +WP-0015-T06 owns the remaining native authority return. An isolated native Authelia/LLDAP test identifies a directory permission prerequisite before the full UC-009 browser journey can be accepted. It does not change the existing personal reset acceptance, prove the currently installed bind's privileges or authorize any live permission change. @@ -95,3 +95,237 @@ expiry/replay, KeyCape, user-engine HTTP and the actual app callback using an explicit synthetic password-manager bind. See native-recovery-browser-review-2026-10-31.md. That new proof does not grant production reset authority or rewrite this API-only receipt's scope. + + +## Owner return assessed on October 31 + +The bind split and its post-split restore drill are now complete under +NK-WP-0054-T05. Their checks cover sign-in and MFA, not password replacement. +The founder receipt records svc-authelia as strict-readonly. Current read-only +metadata finds that bind in the ConfigMap and both deployed images exactly match +this isolated proof. No current directory membership or production reset was +queried, so this is an unresolved reset-authority return rather than a live +failure claim. See reset-owner-return-assessment-2026-10-31.json. + +The same owner workplan now carries T09 (completed evidence assessment) and T10 +(supported reset authority, reviewed execution and native/recovery acceptance). +CSOC-WP-0015-T06 consumes T10. T05 remains complete for its original migration +scope. Previous personal reset acceptance does not establish behavior after the +bind changed; no new grant is inferred from either the old approval or the +synthetic password-manager success. Preparation may proceed independently of the +pending application image and backup installation. + + +## Native authority matrix — October 31 + +`scripts/rehearse-reset-authority.py` extends the synthetic investigation without +changing the application, canonical bind helper, declaration or live directory. +It uses the same pinned native images and network-none loopback confinement. +Run with `--output` naming a fresh local JSON receipt; adjacent source manifest +and receipt refuse overwrite. Disabled assertions refuse before Docker access. + +| Synthetic target | Strict-readonly | Password-manager | +| --- | --- | --- | +| Two ordinary users | Reset refused | Reset succeeds | +| Member of `lldap_admin` | Reset refused | Reset refused | +| Application-admin group member, outside `lldap_admin` | Reset refused | Reset succeeds | +| Other read-only service account, direct directory password API | Refused | Password changes | + +The first four user cases are tested through actual Authelia reset/link flows +and direct directory password changes. Old/replacement password authentication +confirms every outcome; HTTP 200 is insufficient. Unverified requests and consumed +link replay refuse. Service interactive login through Authelia is excluded by the +filter in every scenario. This does not prohibit direct LLDAP web login, and that +filter does not constrain a compromised bind credential's directory password API. + +For each role, seven valid directory mutations refuse: create/delete user, +edit another user's profile, create/delete group, self-promote to directory admin, +and remove another administrator's membership. Matching admin controls succeed, +and complete user/group metadata remains unchanged after the denied operations. +This bounds the tested operations; it does not enumerate every API right. + +The LLDAP-admin restriction agrees with the pinned upstream +[LLDAP v0.6.2 integration guidance](https://github.com/lldap/lldap/blob/v0.6.2/README.md#general-configuration-guide). +The application-admin and service results matter to admission: a password-manager +role is not restricted to Coulomb users or to human accounts. It must not be +presented as restoring all prior reset behavior. No actual account's current +classification has been inferred from these fixtures. + +Owner T11 records this completed rehearsal; T10 retains the supported runtime path. +Before an executable proposal, obtain owner-authorized, value-free classification +of the intended reset subjects and current bind memberships. Review a service-owned +password-write grant against SI-3 and its complete non-LLDAP-admin reach, and, if the accepted subject is a directory administrator, a +supported recovery route for that subject. Preserve the +existing human-admin separation and MFA requirements. A role change alone is not +a complete proposal, and no runtime grant or policy exception is approved here. + +Evidence: reset-authority-matrix-2026-10-31.json and its source manifest; +reset-authority-validation-2026-10-31.json. Ten direct password cases, eight native +Authelia flows, fourteen unrelated-write denials and fourteen matching admin +controls pass. All disposable containers are removed. Both original native reset +scenarios still pass; the full `make check` gate passes 546 tests with zero +failures/errors/skips. These checks add neither productive acceptance nor a +current membership observation. Existing expiry/browser/app proof retains its +separate scope; this matrix does not retest the complete issuer/MFA journey. + + +## Current-role observation for the accepted account + +The October6 acceptance record names **tegwick** +(`history/20261006T185411Z-personal-workspace-acceptance.md`). Decision +8770d4ea-8538-4f88-a9f8-8bcfb0542603 corrects the earlier inference that a new route +for all directory administrators must precede this consumer's remedy. Check +tegwick's current membership first. If it is an LLDAP administrator, its supported +recovery path is required; otherwise retain the administrator exclusion under +existing NetKingdom recovery ownership. The admitted password-write scope still +requires explicit review. This preserves the accepted workflow and adds no grant. + +The fixed-subject observer is prepared and tested (owner T12); actual observation +is owner T13. Run these commands in your own workstation terminal: + +```bash +install -d -m 700 /home/worsch/.local/state/net-kingdom/reset-authority +python3 /home/worsch/net-kingdom/sso-mfa/k8s/lldap/reset-authority-observe.py \ + --receipt /home/worsch/.local/state/net-kingdom/reset-authority/roles-20261031.json +``` + +Enter the **human admin account's password** at the hidden prompt. The tool already +selects tegwick, svc-authelia and admin; it asks for no username, token or backup +credential. Do not paste the password into chat. Return only its JSON result. +The existing NetKingdom bind helper uses this human-held input for authenticated +reads; the new observer reuses its bounded transport without credential discovery. + +Expected status is `current_reset_roles_observed`. It creates a mode0600 local +receipt, signs in as admin and performs three read queries. It changes no group, +password, declaration or deployment, sends no reset email, and grants nothing. +TLS failures and unexpected/missing users refuse; do not bypass verification. +An existing receipt refuses without overwrite. A successful result proves current +role metadata only, not Authelia reset success, app authorization or acceptance. + +Evidence: reset-observer-preparation-2026-10-31.json. The observer and shared +transport pass92 checks; four native owner cases pass and the final observer node +passes with unchanged directory metadata and verified fixture cleanup. Production +observation has not been performed during preparation. + + +### Attended observation received + +The founder completed the read at12:57:44Z on October31. Independent local receipt +verification confirms its private custody, exact subject/endpoint and all four +prepared source hashes. svc-authelia has only strict-readonly and no additional +groups. tegwick has no built-in directory privilege role and one other group. +Thus the directory-admin exclusion does not block the accepted personal flow on +this observed state. Owner T13 is complete; T10 prepares the reviewed transition, +including the full non-admin reach established by the native matrix. No grant, +reset, app authorization or productive acceptance is contained in this receipt. +Do not rerun the completed observer command. Evidence: reset-role-observation- +2026-10-31.json and reset-role-return-verification-2026-10-31.json. + + +## Reviewed role-transition proposal — 2026-10-31 + +**Prepared, not admitted or executed.** Owner preparation is NK-WP-0054-T14; +founder admission/execution is T15; declaration alignment and productive reset plus +fresh identity restore acceptance are T16. T10 and CSOC-WP-0015-T06 remain open. + +Proposal: `sha256:753f50f139a0d090a8b64ba15174f2a7e8b0e639eb0dd01d17783476b5a3759f`. Exact controller and plan are +`/home/worsch/net-kingdom/sso-mfa/k8s/lldap/reset-role-transition.py` and +`reset-role-plan.json`. The digest binds the controller, existing transport and +observer sources, current role observation, native rights matrix, pinned runtime +identities/configuration and final validation. No new resources or recurring +capacity are requested; no credential rotation, service rename or restart is +included. Both pinned workloads were Ready when observed. + +### Decision and scope + +The proposed change replaces **only svc-authelia's strict-readonly membership** +with **lldap_password_manager**. This restores the native password-write capability +needed by the accepted tegwick recovery flow. The role can change passwords for +**all non-LLDAP-admin directory identities**, including other service accounts and +application administrators outside lldap_admin. It is not limited to Coulomb or to +human accounts. LLDAP administrators are excluded. Seven tested unrelated writes +remain refused; this is not an exhaustive rights audit. Both roles already permit +own-profile/password operations in the pinned [LLDAP access-control source](https://github.com/lldap/lldap/blob/v0.6.2/crates/auth/src/access_control.rs). + +The same dedicated SERVICE account and single Authelia consumer remain. The plan +contains the proposed declaration: directory read USE plus narrowly described +password ADMINISTER scope, without the read-only bind convention. SI-3 permits a +service-owned write account with its narrowest working scope. The canonical +read-only declaration has not been changed to imply approval. Following a verified +grant, T16 must align it and existing bind verification/repair expectations. Do +not rerun the historical read-only bind migration as reset verification. + +**Rollback limit:** membership rollback restores fresh Authelia reset denial, but +it does not revoke previously issued HTTP tokens or undo changed user passwords. +A native test proves a token obtained during the grant can still change an eligible +password after membership rollback; a freshly issued token cannot. The pinned +[LLDAP token implementation](https://github.com/lldap/lldap/blob/v0.6.2/server/src/auth_service.rs) +embeds groups in a token with a one-day expiry. Treat retained token authority as +lasting up to 24 hours unless separately revoked. This packet includes no token +revocation or incident-containment operation. Admission includes this limitation. + +### Execution and recovery + +Run in the founder's workstation terminal only after accepting the scope above: + +```bash +python3 /home/worsch/net-kingdom/sso-mfa/k8s/lldap/reset-role-transition.py \ + --execute --expect-digest sha256:753f50f139a0d090a8b64ba15174f2a7e8b0e639eb0dd01d17783476b5a3759f +``` + +The hidden prompt requires the human admin password. Never paste it into chat. +After reviewing the printed scope, enter exactly: + +```text +GRANT sha256:753f50f139a0d090a8b64ba15174f2a7e8b0e639eb0dd01d17783476b5a3759f +``` + +The controller checks the exact live cluster/namespace/deployment identities, +images and Authelia configuration before private authentication and again after +confirmation. It rechecks the human admin role, tegwick's non-admin classification +and svc-authelia's exact baseline before writing. The old role is removed before +the new one is added; a brief directory lookup failure is possible. Every effect +has a durable intent record. All user IDs and group memberships are compared after +each step; unknown concurrent changes fail verification. Profile fields/passwords +are not copied into the receipt or claimed as independently compared. + +Success is `password_manager_role_verified`. Paste only the returned JSON; Codex +will verify the durable mode0600 receipt in +`~/.local/state/net-kingdom/reset-authority/753f50f139a0d090a8b64ba15174f2a7e8b0e639eb0dd01d17783476b5a3759f/journal.json`. +An existing attempt refuses replay. Do not delete its state or repeat the grant. +If the operation is interrupted or reports uncertain status, restore read-only +membership with this separate attended recovery: + +```bash +python3 /home/worsch/net-kingdom/sso-mfa/k8s/lldap/reset-role-transition.py \ + --recover --expect-digest sha256:753f50f139a0d090a8b64ba15174f2a7e8b0e639eb0dd01d17783476b5a3759f +``` + +Confirm `RECOVER sha256:753f50f139a0d090a8b64ba15174f2a7e8b0e639eb0dd01d17783476b5a3759f`. For an already completed successful +grant, use `--rollback` instead of `--recover` and confirm +`ROLLBACK sha256:753f50f139a0d090a8b64ba15174f2a7e8b0e639eb0dd01d17783476b5a3759f`. Both restore only the role and retain +previous journal versions. They preserve unrelated subsequent memberships and +user password changes. Recovery tolerates workload unready status but still +requires pinned identities, images and configuration; runtime drift requires +review, not a bypass. If the directory itself is unreachable, recovery cannot +claim completion until the owner restores access. These are role restoration +commands with the token limitation above, not full credential revocation. + +### Evidence and remaining acceptance + +25 unit checks and four native controller checks pass with no skips/failures. +The native checks include real process termination immediately after removal and +addition, successful recovery without replay, and the retained-token boundary. +A separate confined native rehearsal uses **one running Authelia process** through +read-only → password-manager → read-only: reset refuses, succeeds, then refuses; +old/new passwords and consumed challenge denial are checked each time. No restart +is needed in this tested configuration. Synthetic fixtures and generated passwords +are removed; no provider SMTP or productive password reset was used. + +Evidence: `reset-transition-validation-2026-10-31.json`, +`reset-role-refresh-2026-10-31.json` and its source manifest. A successful role +receipt still leaves T16: source alignment, productive tegwick reset/app/MFA and +fresh identity backup/restore evidence. It does not complete the four goal workplans. + +The final consumer `make check` gate passes546 tests (zero failures/skips;69 +existing warnings). Receipt: reset-transition-consumer-gate-2026-10-31.json. diff --git a/docs/release/operator-handoff-check-2026-10-31.json b/docs/release/operator-handoff-check-2026-10-31.json new file mode 100644 index 0000000..dac6aeb --- /dev/null +++ b/docs/release/operator-handoff-check-2026-10-31.json @@ -0,0 +1,55 @@ +{ + "schema": "coulomb.operator-handoff-check.v1", + "recorded_at": "2026-10-31T16:35:39.718467+00:00", + "custody_preflight": { + "status": "completed_evidence_seal_preflight_verified", + "proposal_digest": "sha256:fa2763d1dcbe92fe638a29c9a7a7d51c03d30849c4232f2d43f81ef705bae611", + "entries": 8, + "modified": false, + "historical_operations_terminal": true + }, + "route_preflight": { + "schema": "rapp-telemetry.backup-route-execution.v1", + "execution_digest": "sha256:4d8481061dc9ce6835169defc96887945052332a0db8d4234ae84032d63cc328", + "proposal_digest": "sha256:4bcad54f7b8e1e77b94112e452cbb58eaf8e882dc10305129c5f9ca69f014a66", + "status": "preflight_verified", + "timers_activated": false, + "notification_sent_by_helper": false, + "production_data_restored": false + }, + "native_receipts_present": { + "custody": false, + "route": false, + "inactive_installation": false + }, + "owner_tasks": { + "RPF-WP-0068-T03": "wait", + "RPF-WP-0068-T04": "todo", + "NK-reset-role-transition": "wait", + "CSOC-WP-0012-T07": "wait", + "CSOC-WP-0012-T08": "wait" + }, + "decision_queue": [ + { + "id": "3ec2f98e-9db3-47d9-bc1a-0387f980b81c", + "title": "Seal eight completed backup-evidence entries (fa2763d1)", + "status": "escalated", + "decision_type": "pending", + "decided_by": null, + "escalation_note": "Auto-escalated per constitution \u00a74: this pending decision touches financial or legal territory and requires explicit human approval before action." + }, + { + "id": "5fa7234f-2a41-4eed-b4fc-9384f493572b", + "title": "Deploy recurring Coulomb backup alert route (4d848106)", + "status": "escalated", + "decision_type": "pending", + "decided_by": null, + "escalation_note": "Auto-escalated per constitution \u00a74: this pending decision touches financial or legal territory and requires explicit human approval before action." + } + ], + "new_approval_recorded": false, + "operator_execution_performed": false, + "message_sent": false, + "timers_activated": false, + "existing_destinations_and_schedule_preserved": true +} diff --git a/docs/release/ordered-list-fidelity-2026-10-31.json b/docs/release/ordered-list-fidelity-2026-10-31.json new file mode 100644 index 0000000..cbcbd89 --- /dev/null +++ b/docs/release/ordered-list-fidelity-2026-10-31.json @@ -0,0 +1,42 @@ +{ + "schema": "coulomb.ordered-list-fidelity.v1", + "verified_at": "2026-10-31T17:20:32.943192+00:00", + "source_articles_sha256": "f33967c79c0f8a60a7d504f5d5e4082f8bc9e3c34f6a3160cb200fd380831d62", + "prior_audit_sha256": "3926e24a3b903f5e1d1c7133dc5f9df1e958183432c5131949ce838eb60acf1e", + "current_audit_sha256": "d8f382e24d7adfe3c43dc71f32f8f6e647bf68239cbcdc6769a6c8fb85d666b9", + "records": 245, + "rendered_cards_verified": 242, + "source_bodies_unchanged": true, + "missing_bodies_retained": 3, + "ordered_list_starts_restored": 15, + "affected_records": 4, + "markup_change_cases_before": 6, + "markup_change_cases_after": 3, + "remaining_removed_tags": { + "topic": 3 + }, + "remaining_removed_attributes": [ + { + "tag": "topic", + "attribute": "name", + "count": 2 + } + ], + "private_review_directory": "~/.config/coulomb-social/auth/exports/rendering-review-20261031-numbering", + "source_hashes": { + "coulomb_social/apps/spaces/content.py": "5d6bb5c800b3e776fff385fa73284f4916403676dc2a8bc06b8fadc7d2387633", + "tests/test_rendering_audit.py": "ae092753f4d1d006bb88f1ce11581975c105c5231819f9409ddaaf2fc83d32f5" + }, + "validation": { + "focused_passed": 14, + "full_gate_passed": 650, + "full_gate_warnings": 69, + "failures": 0, + "errors": 0, + "skipped": 0, + "full_gate_seconds": 91.85 + }, + "production_deployed": false, + "original_bubble_render_compared": false, + "productive_fidelity_accepted": false +} diff --git a/docs/release/platform-mail-sender-review-2026-10-31.md b/docs/release/platform-mail-sender-review-2026-10-31.md index 6ab3b7d..fdc546c 100644 --- a/docs/release/platform-mail-sender-review-2026-10-31.md +++ b/docs/release/platform-mail-sender-review-2026-10-31.md @@ -72,8 +72,9 @@ applies to Authelia, not this additional consumer. SMTP AUTH proof must not subm mail. The application sender migration needs a separate supervised configuration executor and independent postflight after the scoped projection is Ready. -This candidate is **not execution-ready**: no owner admission helper/receipt or -supervised rollout packet exists for this new consumer yet. Resource-version and +This candidate is **not execution-ready**: the owner admission helper is now +prepared under RPF-WP-0068, but its native custody receipt and the separate +supervised sender rollout packet remain absent. Resource-version and identity tests must be refreshed and re-reviewed at execution time. Do not apply these individual patches by hand as a substitute for that packet. @@ -129,3 +130,20 @@ its global-recipient effect and release policy, and agree retention. It must ret proof of why suppression changed and refuse unrelated/uncertain records. Productive inbox access, selected-message authentication/correlation and the operator response remain acceptance gates alongside the sender rollout. + +## Sender transition owner follow-through + +EMAIL-WP-0010 makes the separate configuration executor and native return explicit +owner tasks. Its completed T01 records a fresh six-table native ledger observation +at15:16:41Z, with no uncertain outcomes. Successful invitations may retain an +in_flight attempt row, so the observer follows the application rule that a delivery +receipt takes precedence. Fingerprints and counts share one read-only transaction; +the receiver remained live and this is not a drain or execution-time backup. + +The source has no explicit SIGTERM drain handler. Remaining owner T02 must provide +a concrete supervised executor, fresh recoverable point, durable phase journal, +native postflight and recovery that preserves uncertain/accepted sends and later +writes. Existing admission, image, source and custody packets remain unchanged. +Evidence: `email-connect/docs/sender-transition-boundary-2026-10-31.md` and +`sender-ledger-native-2026-10-31.json` in the owner repository. The owner tests +report192passed/21existing optional-harness skips; all12new observer cases pass. diff --git a/docs/release/productive-copy-move-return-2026-11-01.json b/docs/release/productive-copy-move-return-2026-11-01.json new file mode 100644 index 0000000..a9a0c6d --- /dev/null +++ b/docs/release/productive-copy-move-return-2026-11-01.json @@ -0,0 +1,21 @@ +{ + "schema": "coulomb.productive-copy-move-return.v1", + "review_date": "2026-11-01", + "helm_revision": 21, + "user": "Bernd", + "statement": "The step 2 of the walkthrough seems fine.", + "status": "owner_copy_move_workflow_accepted", + "review_guide": "docs/usecases/productive-content-acceptance-session.md#2-independent-copy-and-move--uc-004", + "accepted_scope": [ + "Copy a page into another space", + "Edit the copy and detach its text attachment while the original body and downloadable file remain unchanged", + "Move another duplicate with its content, Visual and downloads intact", + "Previously saved address reaches the moved destination for the authorized owner" + ], + "remaining_scope": [ + "Moved destination and old-address denial for a participant lacking destination access (step3)", + "Sharing and revocation (step3)", + "Account-local portability and exit/succession semantics (step4)" + ], + "complete_uc004_permission_verdict_inferred": false +} diff --git a/docs/release/reset-authority-matrix-2026-10-31.json b/docs/release/reset-authority-matrix-2026-10-31.json new file mode 100644 index 0000000..fddbf7e --- /dev/null +++ b/docs/release/reset-authority-matrix-2026-10-31.json @@ -0,0 +1,265 @@ +{ + "schema": "coulomb.native-reset-authority-matrix.v1", + "observed_at": "2026-10-31T12:37:24.533367+00:00", + "data": "synthetic", + "status": "native_authority_matrix_verified", + "images": { + "lldap": "sha256:9e605a66c02514bfcffd1b67cafb1e98d50992216bb2871d7ae44622047dd09d", + "authelia": "sha256:bd97cff4fcbf715b5ff1f9ae286afbe6033afce385302520b0368122d43a6f54", + "client": "sha256:512819463a1902b43a615beee14be7bf97e6a05e016385082b9380692ad1a8cd" + }, + "production_access": false, + "provider_smtp_contacted": false, + "owner_runtime_or_permission_change_performed": false, + "no_ports_host_mounts_or_external_routes": true, + "backend_container_flags_verified_before_fixture_import": true, + "source_manifest_sha256": "c982e3a47d9e3aec5095a3c11fea7260fd4f5cb6db57b827b915988b432a18d0", + "target_classes": { + "alice": "ordinary user", + "bob": "other ordinary user", + "operator": "lldap_admin member", + "appoperator": "application-admins member, not lldap_admin", + "svc-other": "other service, lldap_strict_readonly" + }, + "findings": { + "scenarios": [ + { + "bind_role": "lldap_strict_readonly", + "directory_writes": { + "positive_controls": 7, + "refused_mutations": [ + "create_user", + "edit_other_user", + "delete_user", + "create_group", + "delete_group", + "self_promote_admin", + "remove_admin_membership" + ], + "directory_metadata_unchanged": true + }, + "direct_password_changes": [ + { + "target": "alice", + "password_changed": false, + "old_password_accepted": true, + "new_password_accepted": false + }, + { + "target": "bob", + "password_changed": false, + "old_password_accepted": true, + "new_password_accepted": false + }, + { + "target": "operator", + "password_changed": false, + "old_password_accepted": true, + "new_password_accepted": false + }, + { + "target": "appoperator", + "password_changed": false, + "old_password_accepted": true, + "new_password_accepted": false + }, + { + "target": "svc-other", + "password_changed": false, + "old_password_accepted": true, + "new_password_accepted": false + } + ], + "authelia_resets": [ + { + "target": "alice", + "status": "reset_refused", + "only_loopback_interface": true, + "external_routes_absent": true, + "old_password_initially_accepted": true, + "new_password_initially_refused": true, + "native_first_factor": true, + "interactive_service_login_refused": true, + "reset_http_status": 200, + "reset_response_status": "KO", + "native_identity_finish": true, + "reset_without_verified_challenge_refused": true, + "consumed_identity_token_replay_refused": true, + "old_password_accepted_after": true, + "new_password_accepted_after": false + }, + { + "target": "bob", + "status": "reset_refused", + "only_loopback_interface": true, + "external_routes_absent": true, + "old_password_initially_accepted": true, + "new_password_initially_refused": true, + "native_first_factor": true, + "interactive_service_login_refused": true, + "reset_http_status": 200, + "reset_response_status": "KO", + "native_identity_finish": true, + "reset_without_verified_challenge_refused": true, + "consumed_identity_token_replay_refused": true, + "old_password_accepted_after": true, + "new_password_accepted_after": false + }, + { + "target": "operator", + "status": "reset_refused", + "only_loopback_interface": true, + "external_routes_absent": true, + "old_password_initially_accepted": true, + "new_password_initially_refused": true, + "native_first_factor": true, + "interactive_service_login_refused": true, + "reset_http_status": 200, + "reset_response_status": "KO", + "native_identity_finish": true, + "reset_without_verified_challenge_refused": true, + "consumed_identity_token_replay_refused": true, + "old_password_accepted_after": true, + "new_password_accepted_after": false + }, + { + "target": "appoperator", + "status": "reset_refused", + "only_loopback_interface": true, + "external_routes_absent": true, + "old_password_initially_accepted": true, + "new_password_initially_refused": true, + "native_first_factor": true, + "interactive_service_login_refused": true, + "reset_http_status": 200, + "reset_response_status": "KO", + "native_identity_finish": true, + "reset_without_verified_challenge_refused": true, + "consumed_identity_token_replay_refused": true, + "old_password_accepted_after": true, + "new_password_accepted_after": false + } + ] + }, + { + "bind_role": "lldap_password_manager", + "directory_writes": { + "positive_controls": 7, + "refused_mutations": [ + "create_user", + "edit_other_user", + "delete_user", + "create_group", + "delete_group", + "self_promote_admin", + "remove_admin_membership" + ], + "directory_metadata_unchanged": true + }, + "direct_password_changes": [ + { + "target": "alice", + "password_changed": true, + "old_password_accepted": false, + "new_password_accepted": true + }, + { + "target": "bob", + "password_changed": true, + "old_password_accepted": false, + "new_password_accepted": true + }, + { + "target": "operator", + "password_changed": false, + "old_password_accepted": true, + "new_password_accepted": false + }, + { + "target": "appoperator", + "password_changed": true, + "old_password_accepted": false, + "new_password_accepted": true + }, + { + "target": "svc-other", + "password_changed": true, + "old_password_accepted": false, + "new_password_accepted": true + } + ], + "authelia_resets": [ + { + "target": "alice", + "status": "reset_completed", + "only_loopback_interface": true, + "external_routes_absent": true, + "old_password_initially_accepted": true, + "new_password_initially_refused": true, + "native_first_factor": true, + "interactive_service_login_refused": true, + "reset_http_status": 200, + "reset_response_status": null, + "native_identity_finish": true, + "reset_without_verified_challenge_refused": true, + "consumed_identity_token_replay_refused": true, + "old_password_accepted_after": false, + "new_password_accepted_after": true + }, + { + "target": "bob", + "status": "reset_completed", + "only_loopback_interface": true, + "external_routes_absent": true, + "old_password_initially_accepted": true, + "new_password_initially_refused": true, + "native_first_factor": true, + "interactive_service_login_refused": true, + "reset_http_status": 200, + "reset_response_status": null, + "native_identity_finish": true, + "reset_without_verified_challenge_refused": true, + "consumed_identity_token_replay_refused": true, + "old_password_accepted_after": false, + "new_password_accepted_after": true + }, + { + "target": "operator", + "status": "reset_refused", + "only_loopback_interface": true, + "external_routes_absent": true, + "old_password_initially_accepted": true, + "new_password_initially_refused": true, + "native_first_factor": true, + "interactive_service_login_refused": true, + "reset_http_status": 200, + "reset_response_status": "KO", + "native_identity_finish": true, + "reset_without_verified_challenge_refused": true, + "consumed_identity_token_replay_refused": true, + "old_password_accepted_after": true, + "new_password_accepted_after": false + }, + { + "target": "appoperator", + "status": "reset_completed", + "only_loopback_interface": true, + "external_routes_absent": true, + "old_password_initially_accepted": true, + "new_password_initially_refused": true, + "native_first_factor": true, + "interactive_service_login_refused": true, + "reset_http_status": 200, + "reset_response_status": null, + "native_identity_finish": true, + "reset_without_verified_challenge_refused": true, + "consumed_identity_token_replay_refused": true, + "old_password_accepted_after": false, + "new_password_accepted_after": true + } + ] + } + ], + "cleanup_verified": true + } +} diff --git a/docs/release/reset-authority-matrix-2026-10-31.sources.txt b/docs/release/reset-authority-matrix-2026-10-31.sources.txt new file mode 100644 index 0000000..3dfc54a --- /dev/null +++ b/docs/release/reset-authority-matrix-2026-10-31.sources.txt @@ -0,0 +1,3 @@ +scripts/rehearse-reset-authority.py: 120d3b8d4e54ec87386f9f2d53abe836feab48c7cc883a13e0d4672bcc347b0c +scripts/rehearse-native-password-reset.py: 133c8230c524fbbaf7f66e4872e14b826ed2c5a4f4de3bc47fa86cfef7631d5d +scripts/native_recovery/probe.py: eab3bae55626abd4920eb6aa2e329c83559007c54ac74c3aaa1cf41e84eb6556 diff --git a/docs/release/reset-authority-validation-2026-10-31.json b/docs/release/reset-authority-validation-2026-10-31.json new file mode 100644 index 0000000..31a7150 --- /dev/null +++ b/docs/release/reset-authority-validation-2026-10-31.json @@ -0,0 +1,48 @@ +{ + "schema": "coulomb.reset-authority-validation.v1", + "status": "source_and_isolated_native_verified", + "full_gate": { + "command": "make check", + "tests": 546, + "failures": 0, + "errors": 0, + "skipped": 0, + "junit_sha256": "ff99e36bad4e42a1983bdac2749b834a454a22c708065752ecd31f8be5ac963a" + }, + "native_matrix": { + "roles": 2, + "direct_password_cases": 10, + "authelia_reset_cases": 8, + "unrelated_write_denials": 14, + "valid_admin_write_controls": 14, + "cleanup_verified": true + }, + "legacy_native_reset_regression": "passed both original role scenarios", + "cli_refusals": [ + "disabled assertions", + "existing receipt", + "existing source manifest" + ], + "current_live_membership_verified": false, + "production_reset_verified": false, + "authority_grant_approved": false, + "sources": [ + { + "path": "scripts/rehearse-reset-authority.py", + "sha256": "120d3b8d4e54ec87386f9f2d53abe836feab48c7cc883a13e0d4672bcc347b0c" + }, + { + "path": "scripts/native_recovery/probe.py", + "sha256": "eab3bae55626abd4920eb6aa2e329c83559007c54ac74c3aaa1cf41e84eb6556" + }, + { + "path": "docs/release/reset-authority-matrix-2026-10-31.json", + "sha256": "89c555ac2c580153bd81ae03c662d024a0cdbeba173f93e6ebfa041a1a0261c7" + }, + { + "path": "docs/release/reset-default-regression-2026-10-31.json", + "sha256": "82efb79154b5e5003ce4b821ebeba73deb4375a13f4df93eddbd3b8908e705ea" + } + ], + "upstream_reference": "https://github.com/lldap/lldap/blob/v0.6.2/README.md#general-configuration-guide" +} diff --git a/docs/release/reset-default-regression-2026-10-31.json b/docs/release/reset-default-regression-2026-10-31.json new file mode 100644 index 0000000..4879627 --- /dev/null +++ b/docs/release/reset-default-regression-2026-10-31.json @@ -0,0 +1,62 @@ +{ + "schema": "coulomb.native-reset-prerequisite.v1", + "data": "synthetic", + "status": "native_bind_role_reset_boundary_verified", + "observed_at": "2026-10-31T12:36:56.223367+00:00", + "images": { + "lldap": "sha256:9e605a66c02514bfcffd1b67cafb1e98d50992216bb2871d7ae44622047dd09d", + "authelia": "sha256:bd97cff4fcbf715b5ff1f9ae286afbe6033afce385302520b0368122d43a6f54", + "probe": "sha256:512819463a1902b43a615beee14be7bf97e6a05e016385082b9380692ad1a8cd" + }, + "production_access": false, + "browser_app_or_mfa_acceptance": false, + "provider_smtp_contacted": false, + "notifier": "private_tmpfs_file", + "no_ports_host_mounts_or_external_routes": true, + "backend_container_flags_verified_before_fixture_import": true, + "owner_runtime_or_permission_change_performed": false, + "findings": { + "scenarios": [ + { + "bind_role": "lldap_strict_readonly", + "status": "reset_refused", + "only_loopback_interface": true, + "external_routes_absent": true, + "old_password_initially_accepted": true, + "new_password_initially_refused": true, + "native_first_factor": true, + "interactive_service_login_refused": true, + "reset_http_status": 200, + "reset_response_status": "KO", + "native_identity_finish": true, + "reset_without_verified_challenge_refused": true, + "consumed_identity_token_replay_refused": true, + "old_password_accepted_after": true, + "new_password_accepted_after": false, + "directory_group_creation_refused": true, + "native_reset_notification_present": true + }, + { + "bind_role": "lldap_password_manager", + "status": "reset_completed", + "only_loopback_interface": true, + "external_routes_absent": true, + "old_password_initially_accepted": true, + "new_password_initially_refused": true, + "native_first_factor": true, + "interactive_service_login_refused": true, + "reset_http_status": 200, + "reset_response_status": null, + "native_identity_finish": true, + "reset_without_verified_challenge_refused": true, + "consumed_identity_token_replay_refused": true, + "old_password_accepted_after": false, + "new_password_accepted_after": true, + "directory_group_creation_refused": true, + "native_reset_notification_present": true + } + ], + "cleanup_verified": true + }, + "source_manifest_sha256": "1f3977f01757519dd5d08007b4baffa555f2cad44d9301d5dd484ded46f274e9" +} diff --git a/docs/release/reset-default-regression-2026-10-31.sources.txt b/docs/release/reset-default-regression-2026-10-31.sources.txt new file mode 100644 index 0000000..5cbc725 --- /dev/null +++ b/docs/release/reset-default-regression-2026-10-31.sources.txt @@ -0,0 +1,2 @@ +scripts/rehearse-native-password-reset.py: 133c8230c524fbbaf7f66e4872e14b826ed2c5a4f4de3bc47fa86cfef7631d5d +scripts/native_recovery/probe.py: eab3bae55626abd4920eb6aa2e329c83559007c54ac74c3aaa1cf41e84eb6556 diff --git a/docs/release/reset-observer-preparation-2026-10-31.json b/docs/release/reset-observer-preparation-2026-10-31.json new file mode 100644 index 0000000..dde7d60 --- /dev/null +++ b/docs/release/reset-observer-preparation-2026-10-31.json @@ -0,0 +1,77 @@ +{ + "schema": "netkingdom.reset-observer-preparation.v1", + "status": "tested_ready_for_attended_read", + "owner_task": "NK-WP-0054-T12", + "operator_return_task": "NK-WP-0054-T13", + "consumer_task": "CSOC-WP-0015-T06", + "accepted_subject": "tegwick", + "acceptance_source": { + "repo": "coulomb-social", + "path": "history/20261006T185411Z-personal-workspace-acceptance.md", + "sha256": "15ac9e1af4d0cb16b854d1ecf5fa89b634b1479174b02143b5c341cb3b5df2c3" + }, + "technical_binding_decision_id": "8770d4ea-8538-4f88-a9f8-8bcfb0542603", + "queries": [ + "human admin role", + "svc-authelia roles", + "tegwick roles" + ], + "private_input": "human admin password via terminal only", + "directory_mutations_available": false, + "production_observation_performed": false, + "password_reset_attempted": false, + "grant_approved": false, + "target_url": "https://lldap.coulomb.social", + "planned_private_receipt": "/home/worsch/.local/state/net-kingdom/reset-authority/roles-20261031.json", + "gates": { + "unit_and_transport": { + "tests": 92, + "failures": 0, + "errors": 0, + "skipped": 0, + "junit_sha256": "7ee639ba82c7100d7805cc01ca5c2a6c502550eac4abdb252ad3c7d8b1ac232b" + }, + "native_split_regression": { + "tests": 4, + "failures": 0, + "errors": 0, + "skipped": 0, + "junit_sha256": "b35645a61c52f45d0e826e3986145212f45d4abc065026b42bd651f6f4b5fb41" + }, + "final_native_observer": { + "tests": 1, + "failures": 0, + "errors": 0, + "skipped": 0, + "junit_sha256": "251831ae820081aa6e935d08c4e7854a1ad5cf840b8a41ae8af4add5f5406f2a" + } + }, + "native_fixture": "pinned owner LLDAP fixture; synthetic credentials, loopback-only published ports; container and volume removal verified", + "lint": "passed", + "sources": [ + { + "path": "sso-mfa/k8s/lldap/reset-authority-observe.py", + "sha256": "9f19c5551e2a93319c04c1b2fd0ddc0f7757fdff96ad18b81d7ee7bb393b75b9" + }, + { + "path": "sso-mfa/k8s/lldap/lldap_admin.py", + "sha256": "5e39e2eac53846a97cd25151c0a4af1ffc0230db881a6325efb66de6a51bfc36" + }, + { + "path": "identity-provisioner/provisioner.py", + "sha256": "a303a25d3b3876485c796a9ba0836ddaf0a0bec155b609790e028882ad983138" + }, + { + "path": "identity-provisioner/group_provenance.py", + "sha256": "405e08eaaab7c21d618dff4db5970607806f32ab6c461effb81c737e32182b10" + }, + { + "path": "tests/test_reset_authority_observer.py", + "sha256": "348fab447a4182357bf3f7d058b05c7d6088ae084d8e433e76ce5c39c513e152" + }, + { + "path": "sso-mfa/k8s/lldap/test_bind_split.py", + "sha256": "ae69051a3df4596fe220306d88a28b2c12e42fbfe3587e02135e3684eb5f354a" + } + ] +} diff --git a/docs/release/reset-owner-return-assessment-2026-10-31.json b/docs/release/reset-owner-return-assessment-2026-10-31.json new file mode 100644 index 0000000..43ad83b --- /dev/null +++ b/docs/release/reset-owner-return-assessment-2026-10-31.json @@ -0,0 +1,111 @@ +{ + "schema": "coulomb.reset-owner-boundary.v1", + "observed_at": "2026-10-31T12:27:58.389484+00:00", + "configmap_uid": "7af1fa0d-1bd4-47e1-8873-0f620788ef3f", + "configmap_resource_version": "82819079", + "configuration_sha256": "0a8bca50641a4c6f27830f476c2c9f413fc3795711becccb8344ee4d6c376e80", + "bind_is_svc_authelia": true, + "reset_disabled_in_config": false, + "deployments": { + "authelia": { + "uid": "dc40a925-2306-4a3b-89c0-e714187503c3", + "generation": 31, + "observed_generation": 31, + "ready_replicas": 1, + "images": [ + "docker.io/authelia/authelia@sha256:bd97cff4fcbf715b5ff1f9ae286afbe6033afce385302520b0368122d43a6f54" + ] + }, + "lldap": { + "uid": "743a86bd-5cb0-4ca0-9c56-a3438f4746a8", + "generation": 9, + "observed_generation": 9, + "ready_replicas": 1, + "images": [ + "docker.io/lldap/lldap@sha256:9e605a66c02514bfcffd1b67cafb1e98d50992216bb2871d7ae44622047dd09d" + ] + } + }, + "directory_memberships_read_here": false, + "password_reset_attempted": false, + "secret_read": false, + "email_sent": false, + "runtime_changed": false, + "status": "reset_authority_return_incomplete", + "owner_bind_split_task": "NK-WP-0054-T05", + "owner_bind_split_status": "done", + "owner_followthrough": "NK-WP-0054-T10", + "consumer_task": "CSOC-WP-0015-T06", + "owner_membership_receipt_at": "2026-10-30T18:53:16.176849+00:00", + "owner_receipt_bind_groups": [ + "lldap_strict_readonly" + ], + "owner_restore_drill_proves": [ + "independent retrieval", + "directory login", + "Authelia first factor", + "by-user MFA", + "cleanup" + ], + "owner_restore_drill_proves_password_change": false, + "deployed_images_match_isolated_reset_proof": true, + "isolated_readonly_reset_result": "HTTP 200/KO; old password accepted; replacement refused", + "isolated_password_manager_reset_result": "ordinary fixture user changed password; directory group creation refused", + "production_password_reset_failure_claimed": false, + "current_membership_unknown_since_owner_receipt": true, + "prior_personal_reset_acceptance": "preserved as historical; does not prove post-split reset", + "knowledge_orientation": { + "version": "0.2.7", + "digest": "22b7294e24137c0a350764b9cf37f3d4f2e9f35eb7c97a287d9c89477ef24d7a", + "cards": [ + "start", + "identity" + ], + "source_status": "verified-files", + "authority": "orientation only" + }, + "sources": [ + { + "repo": "net-kingdom", + "commit": "55fe8303f31d7476de5180f35c736652f5b4c475", + "path": "workplans/NK-WP-0054-service-identity-policy.md", + "sha256": "1b5411a38583edede8124881825f252b2498335de22559e23d52c787d9c9581a" + }, + { + "repo": "net-kingdom", + "commit": "55fe8303f31d7476de5180f35c736652f5b4c475", + "path": "docs/evidence/2026-10-30-nk54-bind-split/10-verify.json", + "sha256": "b6318e9239623a09d3f6eac1208b0fd7c7a3d3facf55a4c31c8f628454e42ebb" + }, + { + "repo": "net-kingdom", + "commit": "55fe8303f31d7476de5180f35c736652f5b4c475", + "path": "docs/evidence/2026-10-31-identity-restore-drill/drill.json", + "sha256": "47a5a88bdc8a7494d9e5e6687372398367931dca84e38d7de9f6290375caffa2" + }, + { + "repo": "net-kingdom", + "commit": "55fe8303f31d7476de5180f35c736652f5b4c475", + "path": "tools/bootstrap-recovery/isolated-authelia.py", + "sha256": "6139294f6ebdcbaad35934f04b11177d8cf89d134506a730f0c0ebbb3a4967be" + }, + { + "repo": "net-kingdom", + "commit": "55fe8303f31d7476de5180f35c736652f5b4c475", + "path": "sso-mfa/k8s/lldap/bind-split.py", + "sha256": "4decc3bbe65120ff6c4cef5613b9a41a60168ae64ecd4c1380f61a2fcd37b6b3" + }, + { + "repo": "coulomb-social", + "commit": "a786a8050db92807d0f16e637043fbd191ab9335", + "path": "docs/release/native-password-reset-2026-10-30.json", + "sha256": "0666b35773ee3888e12d8d8d877bd8690c9c41b6f98096bf3c5a50195be2294f" + }, + { + "repo": "coulomb-social", + "commit": "a786a8050db92807d0f16e637043fbd191ab9335", + "path": "docs/release/native-recovery-browser-2026-10-31.json", + "sha256": "4d3b644cdf0e76e8ff775a79d8845831f7573a1e2e716e60dd53416343b6e3ba" + } + ] +} diff --git a/docs/release/reset-role-observation-2026-10-31.json b/docs/release/reset-role-observation-2026-10-31.json new file mode 100644 index 0000000..0abb06c --- /dev/null +++ b/docs/release/reset-role-observation-2026-10-31.json @@ -0,0 +1,52 @@ +{ + "schema": "netkingdom.reset-role-observation.v1", + "status": "current_reset_roles_observed", + "observed_at": "2026-10-31T12:57:44.913538+00:00", + "endpoint_sha256": "21b7c63da95eb01178305e49af9b3143295607b8ad68c522d1fa9545b7ef4d9f", + "selected_subject_sha256": "835958a092b4fcddf93eadb83dc2d21519ed1aeab570d04a3d798a4c0f24c0ba", + "human_admin_role_verified": true, + "bind": { + "builtin_roles": { + "lldap_admin": false, + "lldap_password_manager": false, + "lldap_strict_readonly": true + }, + "other_group_count": 0, + "matches_reviewed_service_name_filter": true, + "application_authority_assessed": false + }, + "selected_subject": { + "builtin_roles": { + "lldap_admin": false, + "lldap_password_manager": false, + "lldap_strict_readonly": false + }, + "other_group_count": 1, + "matches_reviewed_service_name_filter": false, + "application_authority_assessed": false + }, + "bind_has_multiple_builtin_roles": false, + "membership_changed": false, + "password_reset_attempted": false, + "grant_approved": false, + "application_access_verified": false, + "credential_values_recorded": false, + "sources": [ + { + "path": "sso-mfa/k8s/lldap/reset-authority-observe.py", + "sha256": "9f19c5551e2a93319c04c1b2fd0ddc0f7757fdff96ad18b81d7ee7bb393b75b9" + }, + { + "path": "sso-mfa/k8s/lldap/lldap_admin.py", + "sha256": "5e39e2eac53846a97cd25151c0a4af1ffc0230db881a6325efb66de6a51bfc36" + }, + { + "path": "identity-provisioner/provisioner.py", + "sha256": "a303a25d3b3876485c796a9ba0836ddaf0a0bec155b609790e028882ad983138" + }, + { + "path": "identity-provisioner/group_provenance.py", + "sha256": "405e08eaaab7c21d618dff4db5970607806f32ab6c461effb81c737e32182b10" + } + ] +} diff --git a/docs/release/reset-role-refresh-2026-10-31.json b/docs/release/reset-role-refresh-2026-10-31.json new file mode 100644 index 0000000..81b4e98 --- /dev/null +++ b/docs/release/reset-role-refresh-2026-10-31.json @@ -0,0 +1,76 @@ +{ + "schema": "coulomb.native-reset-role-refresh.v1", + "observed_at": "2026-10-31T13:23:16.597535+00:00", + "data": "synthetic", + "status": "native_role_refresh_verified", + "images": { + "lldap": "sha256:9e605a66c02514bfcffd1b67cafb1e98d50992216bb2871d7ae44622047dd09d", + "authelia": "sha256:bd97cff4fcbf715b5ff1f9ae286afbe6033afce385302520b0368122d43a6f54", + "client": "sha256:512819463a1902b43a615beee14be7bf97e6a05e016385082b9380692ad1a8cd" + }, + "production_access": false, + "provider_smtp_contacted": false, + "owner_runtime_or_permission_change_performed": false, + "no_ports_host_mounts_or_external_routes": true, + "backend_container_flags_verified_before_fixture_import": true, + "source_manifest_sha256": "f3c6d7b5a286d15d3049bba7e4c543052388f6a95a5e3254414f3b51da1285d0", + "findings": { + "scenarios": [ + { + "bind_role": "lldap_strict_readonly", + "same_authelia_process": true, + "status": "reset_refused", + "only_loopback_interface": true, + "external_routes_absent": true, + "old_password_initially_accepted": true, + "new_password_initially_refused": true, + "native_first_factor": true, + "interactive_service_login_refused": true, + "reset_http_status": 200, + "reset_response_status": "KO", + "native_identity_finish": true, + "reset_without_verified_challenge_refused": true, + "consumed_identity_token_replay_refused": true, + "old_password_accepted_after": true, + "new_password_accepted_after": false + }, + { + "bind_role": "lldap_password_manager", + "same_authelia_process": true, + "status": "reset_completed", + "only_loopback_interface": true, + "external_routes_absent": true, + "old_password_initially_accepted": true, + "new_password_initially_refused": true, + "native_first_factor": true, + "interactive_service_login_refused": true, + "reset_http_status": 200, + "reset_response_status": null, + "native_identity_finish": true, + "reset_without_verified_challenge_refused": true, + "consumed_identity_token_replay_refused": true, + "old_password_accepted_after": false, + "new_password_accepted_after": true + }, + { + "bind_role": "lldap_strict_readonly", + "same_authelia_process": true, + "status": "reset_refused", + "only_loopback_interface": true, + "external_routes_absent": true, + "old_password_initially_accepted": true, + "new_password_initially_refused": true, + "native_first_factor": true, + "interactive_service_login_refused": true, + "reset_http_status": 200, + "reset_response_status": "KO", + "native_identity_finish": true, + "reset_without_verified_challenge_refused": true, + "consumed_identity_token_replay_refused": true, + "old_password_accepted_after": true, + "new_password_accepted_after": false + } + ], + "cleanup_verified": true + } +} diff --git a/docs/release/reset-role-refresh-2026-10-31.sources.txt b/docs/release/reset-role-refresh-2026-10-31.sources.txt new file mode 100644 index 0000000..8cc15a6 --- /dev/null +++ b/docs/release/reset-role-refresh-2026-10-31.sources.txt @@ -0,0 +1,3 @@ +scripts/rehearse-reset-role-refresh.py: c84751c3ed2893ef25460401ab760f109ac8502b308aae120990d402f85ed42f +scripts/rehearse-native-password-reset.py: 133c8230c524fbbaf7f66e4872e14b826ed2c5a4f4de3bc47fa86cfef7631d5d +scripts/native_recovery/probe.py: eab3bae55626abd4920eb6aa2e329c83559007c54ac74c3aaa1cf41e84eb6556 diff --git a/docs/release/reset-role-return-verification-2026-10-31.json b/docs/release/reset-role-return-verification-2026-10-31.json new file mode 100644 index 0000000..c9bdef0 --- /dev/null +++ b/docs/release/reset-role-return-verification-2026-10-31.json @@ -0,0 +1,18 @@ +{ + "schema": "coulomb.reset-role-return-verification.v1", + "status": "attended_role_receipt_verified", + "observed_at": "2026-10-31T12:57:44.913538+00:00", + "private_receipt_sha256": "bde2c45eda858ea317febfabefe1bd254b62c218c66fd4fb0cbec41db61fb788", + "private_file_owner_mode_single_link_verified": true, + "exact_endpoint_and_accepted_subject_verified": true, + "all_four_prepared_source_hashes_verified": true, + "bind_role": "lldap_strict_readonly", + "bind_other_groups": 0, + "accepted_subject_is_lldap_admin": false, + "accepted_subject_has_other_groups": 1, + "grant_approved": false, + "password_reset_attempted": false, + "owner_task": "NK-WP-0054-T13", + "owner_task_id": "278d5ba8-1c2a-58d9-a24e-2233af2c1aef", + "consumer_task": "CSOC-WP-0015-T06" +} diff --git a/docs/release/reset-transition-consumer-gate-2026-10-31.json b/docs/release/reset-transition-consumer-gate-2026-10-31.json new file mode 100644 index 0000000..d1eb5e7 --- /dev/null +++ b/docs/release/reset-transition-consumer-gate-2026-10-31.json @@ -0,0 +1,12 @@ +{ + "schema": "coulomb.reset-transition-consumer-gate.v1", + "command": "make check", + "passed": 546, + "failures": 0, + "skipped": 0, + "warnings": 69, + "elapsed_seconds": 55.15, + "junit_sha256": "717412e60cbe12d52c4a14f124fbe60a59789bc52467c0b938645c2b28c24232", + "proposal_digest": "sha256:753f50f139a0d090a8b64ba15174f2a7e8b0e639eb0dd01d17783476b5a3759f", + "production_grant_performed": false +} diff --git a/docs/release/reset-transition-validation-2026-10-31.json b/docs/release/reset-transition-validation-2026-10-31.json new file mode 100644 index 0000000..c1710c3 --- /dev/null +++ b/docs/release/reset-transition-validation-2026-10-31.json @@ -0,0 +1,74 @@ +{ + "schema": "netkingdom.reset-transition-validation.v1", + "status": "tested_proposal_not_granted", + "unit_checks": 25, + "native_controller_checks": 4, + "failures": 0, + "errors": 0, + "skipped": 0, + "junit_sha256": "26911e26b410c242f3dafb1915301875a4bfd3f5d88770be3b7bec5ce4c5c3d5", + "ruff": "passed", + "native_findings": [ + "grant and rollback preserve all other user IDs and group memberships", + "real process death after removal or addition recovers read-only without replaying grant", + "membership rollback leaves prior HTTP token rights active; freshly issued token loses those rights" + ], + "authelia_findings": "same running process refuses reset before grant, allows after grant, refuses after rollback; three fresh consumed challenges, old/new password checks; all confined containers removed", + "runtime_observation": { + "schema": "netkingdom.reset-runtime.v1", + "kube-system_uid": "a553c742-0115-43d4-99a4-a5ca56fe0786", + "sso_uid": "a4a148d8-92ab-41e6-b975-4e67b73edfc0", + "authelia": { + "uid": "dc40a925-2306-4a3b-89c0-e714187503c3", + "images": [ + "docker.io/authelia/authelia@sha256:bd97cff4fcbf715b5ff1f9ae286afbe6033afce385302520b0368122d43a6f54" + ], + "ready": true + }, + "lldap": { + "uid": "743a86bd-5cb0-4ca0-9c56-a3438f4746a8", + "images": [ + "docker.io/lldap/lldap@sha256:9e605a66c02514bfcffd1b67cafb1e98d50992216bb2871d7ae44622047dd09d" + ], + "ready": true + }, + "authelia_configuration": { + "uid": "7af1fa0d-1bd4-47e1-8873-0f620788ef3f", + "sha256": "0a8bca50641a4c6f27830f476c2c9f413fc3795711becccb8344ee4d6c376e80", + "bind_is_svc_authelia": true + } + }, + "production_membership_changed": false, + "grant_approved": false, + "productive_acceptance": false, + "sources": [ + { + "path": "sso-mfa/k8s/lldap/reset-role-transition.py", + "sha256": "cc405861bb5cc0adf419a1cf98a90872d8395eecbfc7d4d38947cb4008bee217" + }, + { + "path": "sso-mfa/k8s/lldap/reset-role-plan.json", + "sha256": "703e281ee6a17e1ef0a1dffbad35b8025ab3480d51d3fc2c36323f2bc6227106" + }, + { + "path": "tests/test_reset_role_transition.py", + "sha256": "1437761ac60a7aeba16db70ccf07d48a9f5332617a00e7a325e678951985a08e" + }, + { + "path": "sso-mfa/k8s/lldap/test_reset_role_transition_native.py", + "sha256": "13c53e8a757ed0e954ffd28df14283d51dd9d7d3278c1d7d7d5759b419a2a89a" + }, + { + "path": "sso-mfa/k8s/lldap/test_bind_split.py", + "sha256": "ae69051a3df4596fe220306d88a28b2c12e42fbfe3587e02135e3684eb5f354a" + }, + { + "path": "sso-mfa/k8s/lldap/bind-split-rehearsal.py", + "sha256": "9ab873d1e941807fb19a48fb18c3af246a98bf0788660d8d79a14fb0c0e0a067" + }, + { + "path": "docs/evidence/2026-10-31-reset-authority/role-refresh.json", + "sha256": "325f446960134e9510591a2a27f5833bc80fa858d77e6a88082af41307993b65" + } + ] +} diff --git a/docs/release/service-contract-conformance-2026-10-27.md b/docs/release/service-contract-conformance-2026-10-27.md index 4b5a8b0..3968263 100644 --- a/docs/release/service-contract-conformance-2026-10-27.md +++ b/docs/release/service-contract-conformance-2026-10-27.md @@ -7,12 +7,12 @@ evidence from the returns required to finish WP-0012/0014. | Section | Current evidence | Remaining proof | | --- | --- | --- | -| 1: shape/tenancy | Standalone repo, `coulomb_social_db`, dedicated deployment/values in railiance-apps. Space/Member tenant keys and explicit membership joins; tests deny foreign tenant/member access. Helm revision 18 applies migration 0003 and verifies global and tenant slug constraints with unchanged business data. | Reconcile all source grants and prove complete tenant export/consolidation without ambiguous paths; global uniqueness alone does not certify a multi-client migration. | -| 2: identity | OIDC/PKCE behind `apps/identity`; app-local users have unusable passwords. Accepted ADR-0001 specializes the local-account baseline. Historical Case B and personal recovery are recorded in docs/identity/smoke.md and the October 6 receipts. | Current registration Case B and source-cohort identity/grant return. Link the governing State Hub decision for the ADR specialization before claiming §8.3 deviation acceptance; the ADR alone does not establish that link. | -| 3: build/release | Founder storage release verified at Helm revision 18, immutable digest `51281946…`, source `17d0eb6`; all 73 packaged files match. Deployment-owner values and receipt are published at `8434134`, with public health/static and 40 owner/40 outsider checks passing. | Later source changes need their own current-target release acceptance. Old contract §3.2 names Gitea; current delivery uses Forgejo. Resolve the fleet successor provenance rather than silently treating that as identical. | -| 4: operations | Deployment Ready, health endpoint/runbooks. October 29 protected provider-only image/tools/pair recovery proved all tables/files and packaged app views in 30.232s, with independent escrow and retained proof. October 30 read-only native operating preflight identifies missing recurring worker mirror receipt; both timers remain absent/inactive. | Current ops-hub client/environment/endpoint registration evidence; admitted recurring *paired* backup with matching mirrored point/receipt, retention/headroom and received failed/stale signals; post-release stage-1 CRUD/copy/transfer smoke and CSOC-IN-0004 reconciliation. Provider-only recovery excludes operator-account recovery and a fresh Docker daemon. Database-only backup does not cover CONTENT_ROOT. | +| 1: shape/tenancy | Standalone repo, `coulomb_social_db`, dedicated deployment/values in railiance-apps. Space/Member tenant keys and explicit membership joins; tests deny foreign tenant/member access. Revision 18 applied migration 0003; revision 19 preserved both slug constraints, content and migration history. | Reconcile all source grants and complete client attribution/export scope; global uniqueness alone does not certify a multi-client migration. | +| 2: identity | OIDC/PKCE behind `apps/identity`; app-local users have unusable passwords. Accepted ADR-0001 specializes the local-account baseline. Historical Case B and personal recovery are recorded in docs/identity/smoke.md and the October 6 receipts. | Current registration Case B and source-cohort identity/grant return. The authentication choice is now bound to existing founder decision8bbce691 under §8.3 (T14); broader ADR implementation and current productive identity returns retain their own gates. | +| 3: build/release | Founder image release verified at Helm revision 19, immutable digest `522bcd7f…`, source `23ffe2c`; all 80 packaged files match. Owner values, public health/static, 40 owner renders and 40 outsider denials are verified in `identity-export-release-return-2026-10-31.md`. Forgejo successor and identity-decision provenance are resolved under T14. | Content lifecycle candidate `d3f2f5bb…` and proposal `c9c32316…` still await their exact release after backup-source installation. Existing byte equality does not prove reproducible dependencies or later release acceptance. | +| 4: operations | Revision 19 is Ready. Its 07:13 paired point was independently mirrored, retrieved and restored with 13 tables/114 rows/86 files. Existing Prometheus has Kubernetes readiness history; the October 31 read finds no Coulomb scrape target or probe/request metrics in the inspected families. Native retention is 10 days. Custody, route, installation and content-release receipts are still absent at 14:38Z. | Authenticated ops registration; admitted recurring paired backup, retention/headroom and received failed/stale signals; application-level availability/latency measurements over the required windows; post-release CRUD/copy/transfer smoke and CSOC-IN-0004 reconciliation. Kubernetes readiness and a point health check do not prove HTTPS availability or user latency. Provider-only recovery excludes operator-account recovery and a fresh Docker daemon. | | 5: cost | Resource inventory ID `resource:tenant:coulomb:coulomb-social` (not a valid fin-hub client attribution key), dated workload/demand/labor records in docs/resource-evidence; WP-0017 records current requests 15m/176Mi. | Obtain the authoritative client × app × instance mapping and demonstrate its canonical fin-hub key in current finance ingestion and per-instance infrastructure cost report. Planning labor rates and CPU requests are not booked costs. | -| 6: protection | Platform instance isolation, explicit read/write denial, sanitized Markdown, private images, account-local JSON export and successor-aware local exit. October 30 source adds a private operator tenant-dataset ZIP export and schema-derived personal-data inventory; review in client-data-export-review-2026-10-30.md. No credentials copied into these records. | Release and verify coherent current-target client export, resolve external/unattributed data, and agree instance teardown/session/log disposition plus primary/mirror/protected-kit backup expiry (§6.2). Source support does not establish productive offboarding. Productive sharing/portability verdicts and retained-data disposition remain WP-0013. | +| 6: protection | Platform instance isolation, explicit read/write denial, sanitized Markdown, private images, account-local JSON export and successor-aware local exit. Revision 19's packaged tenant export verifies 85 selected files and four record sets on the restored current release point without source mutation. No credentials copied into these records. | Resolve external/unattributed data, private delivery and client acceptance; agree instance teardown/session/log disposition plus primary/mirror/protected-kit backup expiry (§6.2). The coherent export proof does not establish productive offboarding. Productive sharing/portability verdicts remain WP-0013. | The August service-objectives record is historical. Its RPO ≤60min/RTO ≤4h and Forgejo content durability assumptions predate the persistent CONTENT_ROOT @@ -23,6 +23,20 @@ Availability 99.5%, health p99 500ms and page latency objectives remain planning targets, with no current measurement window proving attainment. Preserve dated observations; do not rewrite August measurements as current facts. +The October31 14:39–14:41 read of the existing private Prometheus service returns +one current available replica and 20,160 readiness samples in the queried seven-day +window, with observed available replicas ranging from zero to one. Sample counts +do not prove uninterrupted coverage, and these values are not HTTP success or +latency measurements. No exclusion of planned maintenance or dependency outages +was applied. The inspected `probe_`, `django_`, `traefik_`, `http_request` and +`http_server` metric families are absent, and `up{namespace="coulomb-social"}` +has no series. Runtime retention is10days/10GiB, not the seven-day package default; +no configuration was changed to reconcile that difference. No retained monthly +application measurement or current percentile proof is established. T11 needs +admitted application-level measurement and retained window evidence, alongside +registration, finance and smoke. Native queries and limits: +`existing-telemetry-2026-10-31.json`. + WP-0012 owns the operating/service returns above. WP-0013 owns privacy/content verdicts; WP-0014 owns all-space reconciliation and the separate cohort/apex cutover decision. Existing canonical cross-owner records remain in their owners; @@ -54,3 +68,8 @@ files and four record sets without source changes. This closes the release and coherent-export proof portions of §§3/6, not private delivery/offboarding, recurring service evidence, operator ownership, productive verdicts or successor provenance. Evidence: identity-export-release-return-2026-10-31.md. + +October31 provenance reconciliation closes the two previously missing decision/ +successor links under T14. See service-provenance-binding-2026-10-31.json and the +latest section of service-provenance-reconciliation-2026-10-30.md. All remaining +service registration, finance, operating and productive gates above remain open. diff --git a/docs/release/service-provenance-binding-2026-10-31.json b/docs/release/service-provenance-binding-2026-10-31.json new file mode 100644 index 0000000..fc0d63b --- /dev/null +++ b/docs/release/service-provenance-binding-2026-10-31.json @@ -0,0 +1,157 @@ +{ + "schema": "coulomb.service-provenance-binding.v1", + "observed_at": "2026-10-31T13:38:26.593082+00:00", + "status": "forge_and_identity_provenance_bound", + "owner_task": "CSOC-WP-0012-T14", + "remaining_service_task": "CSOC-WP-0012-T11", + "native_readonly": { + "observed_at": "2026-10-31T13:38:26.593082+00:00", + "deployment": { + "uid": "2dbe661a-d232-4477-bbd1-833813077fbe", + "images": [ + "forgejo.coulomb.social/coulomb/coulomb-social@sha256:522bcd7fce92436765a60c1e4afa2598f3cc40913352153ff1c4319a99ca92ed" + ], + "ready_replicas": 1 + }, + "forge_version": { + "http_status": 200, + "version": "11.0.3+gitea-1.22.0", + "tls_verified": true + } + }, + "source_commit": "23ffe2cf4b214666754a2420558943438e54251a", + "image": "forgejo.coulomb.social/coulomb/coulomb-social@sha256:522bcd7fce92436765a60c1e4afa2598f3cc40913352153ff1c4319a99ca92ed", + "migration_workplan": { + "id": "84e17675-0d15-4268-a8bd-540124d37018", + "record_id": "RAIL-HO-WP-0005", + "hub_status": "finished", + "legacy_disposition_task": "a63147b0-31d5-4705-89ea-40c10faf779f", + "legacy_disposition_hub_status": "done" + }, + "governing_decisions": [ + { + "id": "6bf3700e-f9fa-4e4e-ab11-2c4e0b10d4d3", + "title": "Forgejo hostname and exposure model", + "status": "resolved", + "decision_type": "made", + "description": "WP-0005 T02 needs the production Forgejo hostname, private/public exposure model, and whether legacy Gitea remains reachable during transition.", + "rationale": "the hostname shall be forgejo.coulomb.social. The exposure model is private repos by default. We will use the transition from gitea to forgejo for closing down public access and establishing credential handling to have convenience when working with the repos. Gitea can and should remain reachable during transition.", + "decided_by": "human", + "decided_at": "2026-07-02T19:19:29.160890Z" + }, + { + "id": "804c1d54-e63f-4d88-8d85-a095a7a9e956", + "title": "Forgejo package registry scope", + "status": "resolved", + "decision_type": "made", + "description": "WP-0005 T02/T07 need the initial package registry scope: container images and generic packages only, or also npm, PyPI, Go, Maven, and Helm packages.", + "rationale": "We should support the full range from the start.", + "decided_by": "human", + "decided_at": "2026-07-02T19:21:18.314775Z" + }, + { + "id": "0cc6f748-c5c0-4ff1-9722-f59e6cd03fa5", + "title": "Forgejo cutover and rollback strategy", + "status": "resolved", + "decision_type": "made", + "description": "WP-0005 T02/T11/T12 need the cutover mode, freeze window, staged vs all-at-once migration choice, rollback window, and criteria for retiring or retaining legacy Gitea.", + "rationale": "We will do a staged migration and lock the gitea repos of transitioned repos but keep gitea until everything has been transfered and just then start a 14day trial phase and after that retire gitea to the backup.", + "decided_by": "human", + "decided_at": "2026-07-02T19:26:16.298551Z" + }, + { + "id": "8bbce691-a787-4140-9cd4-df06e8635a27", + "title": "Approve tegwick personal sign-in repair and persistent Coulomb app release", + "status": "resolved", + "decision_type": "made", + "description": "Prepared review: docs/release/personal-account-2026-10-06.md. Restore historical public coulomb-social OIDC client (authorization_code/PKCE, exact app/apex/loopback callbacks, scopes openid profile email groups, historical ordinary AAL1 mfaRequired=false), CAS patch preserving other clients/Secret data; restart shared sso/keycape (in-flight sign-ins may be interrupted). Then fresh apps-pg CNPG backup, Helm atomic upgrade chart0.2.0 with image digest sha256:bf975939e22b9b0a5d9ac05584ae6265ba201169df6dc48e506ec684b30a9c58, retained 2Gi local-path content PVC, one replica/Recreate. Existing account/DB/env Secret references retained. No real-data import or apex cutover. Requires founder exact go: ADMINISTER @ realm:kubernetes/railiance01, activation=APPROVED. Inspection/dry-runs/tests complete; no live mutations performed.", + "rationale": "Founder explicitly replied: Go on and deploy the interruption is not a problem. Approved exact reviewed client restoration, KeyCape restart, fresh database backup and persistent digest-pinned app upgrade. ADMINISTER @ realm:kubernetes/railiance01, activation=APPROVED. No apex cutover or real-data import.", + "decided_by": "Bernd Worsch (founder)", + "decided_at": "2026-10-06T14:53:19.566637Z" + } + ], + "conclusions": { + "forge_successor_binding_resolved": true, + "legacy_registry_string_is_historical": true, + "legacy_retirement_executed_here": false, + "new_runtime_operation_authorized": false, + "identity_deployment_approval_found": true, + "ops_registration_verified": false, + "finance_mapping_verified": false, + "current_slo_attainment_verified": false, + "full_service_conformance": false, + "identity_authentication_choice_bound_to_existing_founder_decision": true, + "all_adr_statements_implemented_or_approved": false + }, + "limitations": [ + "Migration closeout retained Gitea as read-only fallback; no current legacy teardown claim. July2 decision mentions14days; July9 source records30days. This reconciliation establishes successor ownership, not expiry or retirement permission.", + "October6 founder decision approves the exact OIDC client restoration and app release; it is not evidence that every ADR-0001 statement or service-contract demand trigger was separately assessed.", + "Live observations are deployment/version metadata only; existing retained packaged-source equality proof has its original scope." + ], + "sources": [ + { + "path": "the-custodian/canon/standards/business-app-service-contract_v0.1.md", + "sha256": "83d2799d39f957d88aa1d71c3f04f7c2a0e168390249dc409ce149128d44e80c" + }, + { + "path": "the-custodian/canon/standards/coulombcore-production-freeze_v0.1.md", + "sha256": "8ad6aaa96a23b3498e94d350e7bfac12b0cab49736b7e5929b17a870d2fdd96d" + }, + { + "path": "the-custodian/docs/forgejo-production-decisions.md", + "sha256": "0b9174e28cf056a857f209b09d6cc7cf9d1296d8a118992b17290ec3945cb1b0" + }, + { + "path": "railiance-infra/workplans/archived/260714-RAIL-HO-WP-0005-forgejo-production-migration.md", + "sha256": "c93ae0d5f50afed4f625e3534844ee8ff426d12991327c14b86232e0ffa38485" + }, + { + "path": "railiance-apps/helm/coulomb-social-values.yaml", + "sha256": "63a45d1cfb395eff7cfdf21ed2100fb9562b7a2ab6b896afecbebfaabacae547" + }, + { + "path": "coulomb-social/registry/release-units/coulomb-social.yaml", + "sha256": "309102bdee9d0ec0057c5d679be729ebd7aa3477d588962c2e757a38946761f6" + }, + { + "path": "coulomb-social/docs/release/identity-export-release-return-2026-10-31.md", + "sha256": "e78222c650ff3939bb8e8f550e9b75534c9f43badea612a4ea3a2792fba82d01" + }, + { + "path": "coulomb-social/docs/adr/ADR-0001-netkingdom-identity.md", + "sha256": "7f606904418a1693b80cfda2655d4728ab32b636409ab96b1cd30278bb61a7c0" + }, + { + "path": "coulomb-social/docs/release/personal-account-2026-10-06.md", + "sha256": "2eb5f37a6d9165b6166b175de3323a88cc090f89edc8a30a873ef26537c5e3e0" + } + ], + "technical_binding_decision_id": "7b68c574-aa61-4517-bb31-1ecda554c3e8", + "goal_task_projection_readback": { + "workplans": [ + { + "id": "CSOC-WP-0012", + "tasks": 14, + "done": 6 + }, + { + "id": "CSOC-WP-0013", + "tasks": 5, + "done": 2 + }, + { + "id": "CSOC-WP-0014", + "tasks": 7, + "done": 2 + }, + { + "id": "CSOC-WP-0015", + "tasks": 6, + "done": 3 + } + ], + "all_task_statuses_match_source": true, + "progress_event_id": "fb1cb027-4bcd-4844-9626-2e1c0640955c", + "source_and_native_validation_only": true + } +} diff --git a/docs/release/service-provenance-reconciliation-2026-10-30.md b/docs/release/service-provenance-reconciliation-2026-10-30.md index 30d6106..41cd36b 100644 --- a/docs/release/service-provenance-reconciliation-2026-10-30.md +++ b/docs/release/service-provenance-reconciliation-2026-10-30.md @@ -147,3 +147,45 @@ record/mapping and actual per-instance finance report remain required. Native observations and source hashes: `service-owner-boundary-2026-10-31.json` and its `.sources.txt` companion. + + +## October 31: successor and identity decision bindings resolved + +CSOC-WP-0012-T14 records this completed evidence reconciliation. T11 retains the +remaining service contract work. Technical decision +`7b68c574-aa61-4517-bb31-1ecda554c3e8` links existing approvals; it grants no new +runtime action or policy exception. + +The contract's Gitea registry address is historical. Founder decisions +`6bf3700e-f9fa-4e4e-ab11-2c4e0b10d4d3` (Forgejo hostname/exposure), +`804c1d54-e63f-4d88-8d85-a095a7a9e956` (registry scope) and +`0cc6f748-c5c0-4ff1-9722-f59e6cd03fa5` (migration) establish the intended successor. +The active CoulombCore production-freeze canon identifies railiance01 Forgejo as +the canonical forge/registry target. Archived owner RAIL-HO-WP-0005 records completed +cutover and deliberate legacy read-only fallback; State Hub independently returns +finished for that plan and done for its T12. The July2 decision's14-day window and +July9 source's30-day window differ: neither is treated here as permission to delete +legacy state or proof of subsequent teardown. + +Fresh read-only observations at13:38:26Z confirm one Ready Coulomb deployment on +image522bcd7f from `forgejo.coulomb.social/coulomb/coulomb-social`. Its UID, current +release-unit and deployment-owner values match the retained revision19 return. +The anonymous TLS-verified forge version endpoint returns200 and +`11.0.3+gitea-1.22.0`; that compatibility suffix does not make it the retired Gitea +instance. The retained80-file source equality proof keeps its original scope; +this observation does not rebuild the image or prove reproducible dependencies. + +For identity, the missing governing link is the existing founder decision +`8bbce691-a787-4140-9cd4-df06e8635a27`: it explicitly approved the exact historical +NetKingdom OIDC/PKCE client restoration and Coulomb app release. Link this recorded +choice to ADR-0001's authentication specialization under contract§8.3. Requiring +the same product authentication choice to be approved again would disregard that +recorded approval. This does not assert that a§2.4 demand trigger occurred, that +all ADR statements are implemented, or that every authorization/MFA behavior has +been accepted. The later password-reset role grant remains separately unapproved. + +Evidence: `service-provenance-binding-2026-10-31.json` retains selected decisions, +source hashes, native metadata and explicit limits. These two provenance items +are resolved. Authenticated ops registration, actual client/app/instance finance +mapping and current report, current SLO attainment and stage-1 smoke reconciliation +remain T11; no full service conformance is claimed. diff --git a/docs/usecases/fixtures/productive-content-acceptance/attachment-check.txt b/docs/usecases/fixtures/productive-content-acceptance/attachment-check.txt new file mode 100644 index 0000000..3299c5f --- /dev/null +++ b/docs/usecases/fixtures/productive-content-acceptance/attachment-check.txt @@ -0,0 +1,2 @@ +Coulomb attachment acceptance +This file should download unchanged. diff --git a/docs/usecases/fixtures/productive-content-acceptance/manifest.json b/docs/usecases/fixtures/productive-content-acceptance/manifest.json new file mode 100644 index 0000000..e69453b --- /dev/null +++ b/docs/usecases/fixtures/productive-content-acceptance/manifest.json @@ -0,0 +1,14 @@ +{ + "schema": "coulomb.productive-acceptance-samples.v1", + "purpose": "Harmless files for an attended productive review", + "files": { + "attachment-check.txt": { + "bytes": 67, + "sha256": "3dc7fcbfc6f2fa76172a42d61cedcf1b1acbd4a09d1ba4b38057776d864628eb" + }, + "visual-check.png": { + "bytes": 125, + "sha256": "f5cfb696601f2da142f011cc09e82d14febaf444a395dc2c558d1a23e7d1f635" + } + } +} diff --git a/docs/usecases/fixtures/productive-content-acceptance/visual-check.png b/docs/usecases/fixtures/productive-content-acceptance/visual-check.png new file mode 100644 index 0000000..2f43adb Binary files /dev/null and b/docs/usecases/fixtures/productive-content-acceptance/visual-check.png differ diff --git a/docs/usecases/productive-content-acceptance-session.md b/docs/usecases/productive-content-acceptance-session.md new file mode 100644 index 0000000..84a694a --- /dev/null +++ b/docs/usecases/productive-content-acceptance-session.md @@ -0,0 +1,111 @@ +# Productive content acceptance session + +The editor attachment correction imageb58d858a is independently verified at +Helm revision21, completed2026-10-31T23:32:38Z. Reopen any editor tabs from +revision20. Record your review date and actual verdicts. + +Use your normal account at https://app.coulomb.social. Create two private spaces, +`acceptance-a-20261031` and `acceptance-b-20261031`, or choose unused names if those +already exist. These are disposable review spaces. Keep existing workspaces and +their grants intact. Keep the review artifacts until the verdicts are recorded. + +## 1. Authoring and attachments — UC-003 / WP-0013-T06 + +In space A, create a page titled `Acceptance notes`, file name `notes`, and abstract +`Content and attachment review`. Use this body: + +```html +Acceptance notes. + +
    +
  1. Third item
  2. +
  3. Fourth item
  4. +
+ +**Keep this text when adding or removing attachments.** +``` + +Save and reopen. The list should start at3. In Attachments, add +[attachment-check.txt](fixtures/productive-content-acceptance/attachment-check.txt) +and [visual-check.png](fixtures/productive-content-acceptance/visual-check.png). +Download the text file and open the image: the text has two short lines and the +image has blue and yellow halves. Files should remain available after reopening. + +To check a page Visual, use Open image and take the `assets/...` part of its +address as the Visual field in the page editor. Save and check the displayed +image. Edit the title/abstract/body and reopen to check persistence. + +Open the page editor in two tabs. Both should show Attachments with working +downloads. Save a change in the first; then try saving the +older second tab. The second submission must retain your submitted text and ask +you to reopen/merge rather than overwrite the newer page; its Attachments section should remain visible. Return to the current +page and check that its attachments and Visual remain intact. + +Verdicts: `UC-003 accepted/gap` and `attachments accepted/gap`, with any observed +difference. The custom-topic rendering and obsolete-scrap dispositions are already +recorded; they do not need another review. + +## 2. Independent copy and move — UC-004 + +Duplicate the page into space B with file name `copied`. Change its body and +remove its text attachment. Reopen the original in A: its body and downloadable +text file should remain unchanged. The removed copy's earlier revisions retain +the file, as agreed in the retention policy. + +Create another duplicate in A with file name `moving`, retaining both files. +Save its address before moving it to B with file name `moved`. Its content, +Visual and downloads should remain intact. Its old address should reach the +destination for an authorized account; the sharing check below verifies denial +when the destination is private to you. + +Verdict: `UC-004 accepted/gap`, including the incoming-address permission result +once the second participant has checked it. + +## 3. Sharing and revocation — UC-005 + +The user nominated `testuser` as the second participant. The live app had only +one member at preparation. Have `testuser` sign in to the same tenant and +obtain their member ID from Your app data. The same participant can cover the +ungranted, viewer and revoked states in sequence, using their own browser session. + +Before a grant, they try the A page and its direct image/download URLs: access +must be denied. In A's Share space screen, grant their member ID Viewer access. +They should now read and download, but receive denial for direct edit/copy/move +URLs copied from your session. Space B stays private: they must be denied both +the moved page and its old incoming address. + +Revoke their A access. They retry the page, image and saved download URLs: each +must be denied. Review the remaining grants and creator access in Share space. +Sharing does not send email. + +Verdict: `UC-005 accepted/gap`, including the participant's three access states. + +## 4. Account-local portability — UC-006 + +Open https://app.coulomb.social/account/data/ and Export my account records. +Inspect the downloaded JSON privately: profile, identity references and space +memberships should match your local account. Pages/assets remain in their spaces; +this account export is distinct from the complete operator client-data export. + +Review Leaving the app and the ownership/succession explanation. Sole-owned +spaces must gain another active owner before local account removal. Shared +content remains, while platform identity and mailbox remain under their existing +owners. Review these screens with your productive account. Any actual account +removal exercise needs a separately selected disposable member. + +Verdict: `UC-006 accepted/gap`, stating whether export and the displayed exit/ +succession semantics meet your needs. Keep the account export private. + +## Result record + +Record the deployed revision/date and each verdict independently: + +- UC-003 authoring: +- Attachments: editor visibility confirmed; owner copy/removal independence accepted in step2. Reader/revoked access remains step3. +- UC-004 copy/move and old-address permissions: owner workflow accepted2026-11-01 (“The step 2 of the walkthrough seems fine.”). Participant denial through the old address remains step3. +- UC-005 sharing/revocation: +- UC-006 account-local export and exit/succession semantics: + +An unresolved gap or an unperformed participant check stays open. Automated +regression, native postflight and recovered-store proofs remain supporting +evidence and do not supply these user verdicts. diff --git a/docs/usecases/sharing-portability-review.md b/docs/usecases/sharing-portability-review.md index 683d00c..e685972 100644 --- a/docs/usecases/sharing-portability-review.md +++ b/docs/usecases/sharing-portability-review.md @@ -10,6 +10,16 @@ Record the actual release/date with each verdict. Later source changes, includin the attachment interruption fix below, are not implicitly part of that image. Automated checks do not supply these verdicts. +October31 closure update: the reviewed backup route is now executed and +independently verified at revision6. The inactive installer is also founder-executed +and independently verified, including its local capacity profile. A newly identified ordered-list numbering correction +must accompany the attachment fix in a refreshed content release; the historical +lifecycle-only packet below remains evidence, not the final combined proposal. +The combined candidate also includes the user's requested attachment menu, with +author upload/removal and authenticated reader downloads. Uploads are limited to +16 MiB per file. Removal detaches the current association; retained revisions and +their bytes remain. Copies/moves carry independent files with destination access. + | Journey | Review steps | Expected result | | --- | --- | --- | | UC-003: authoring | Create a disposable page with title, abstract, visual and body. Save/reopen; edit and reopen again. Open two edit tabs, save in one, then submit the older tab. | Content remains recognizable. Stale submission preserves your text and asks you to reopen/merge. Verify representative custom Markdown/raw HTML/assets separately; sanitization differences need an explicit verdict. | @@ -37,7 +47,20 @@ Current private index: Source exceptions, including the unassigned record, have a separate private index: `~/.config/coulomb-social/auth/exports/source-exception-review-20261031/index.html`. -In the private index, review the six markup-change cards first. Each shows the +The refreshed **source-candidate** review is now +`~/.config/coulomb-social/auth/exports/rendering-review-20261031-numbering/index.html`. +It restores15ordered-list starting numbers across4records, with all242source +bodies unchanged. Three custom-topic markup cases remain, as do the three +missing bodies and separate resource questions. The original six-case review +above remains unchanged and describes the previous renderer. Neither index +establishes original Bubble fidelity or the deployed image's acceptance. Source +proof: `../release/ordered-list-fidelity-2026-10-31.json`. + +Bernd accepted the three remaining custom-topic cases as “fine for now” on +October31 (decision69ec9380-e5f6-41aa-af88-f697fc1c8a66). This is a rendering +disposition only; missing bodies and unresolved resources remain explicit. + +For other fidelity checks, each card shows the exact captured source beside the current product rendering and provides an explicit link to the original Bubble view. Compare the same record there: meaning and placeholders, line breaks, lists/tables, Visual and any body-linked @@ -47,7 +70,10 @@ accept intended differences before marking the corresponding cohort transferred. Review the body-missing records as source exceptions; never substitute an empty page. Card samples disable resource loading and interaction, so image/resource fidelity needs the separate authorized asset review. Then apply UC-003–006 above -to disposable pages on the exact intended release. +to disposable pages on the exact intended release. Include attachment upload, +download and removal, independent copy/move files, and reader/outsider/revoked +download checks. The new menu has synthetic source/image evidence; its productive +verdict remains CSOC-WP-0013-T06. ## Revision and interrupted-operation lifecycle — WP-0013-T05 @@ -80,3 +106,120 @@ symlinks. Existing retention/incoming-address permission tests remain required. Evidence: `../release/content-lifecycle-validation-2026-10-31.json`. This is source and isolated-fixture evidence; the fix still needs release delivery and productive review. Do not kill or mutate the productive application to perform this review. + +## Historical lifecycle-only packet — superseded by combined release + +The owner packet is `railiance-apps/release/coulomb-content-lifecycle-f306dbf.json`, +proposal `sha256:c9c3231620721ef3ea9b2bf22fc6c62fe3ed8efb91386cf7ec96acc6959dae08`. +It upgrades the verified revision19/image522bcd7f to candidate d3f2f5bb, with no new +migration, resource, authorization grant or content rewrite. The only packaged +application change is attachment journal ordering/durability in pageops.py. + +**Historical preflight refused the then-missing inactive-installation receipt.** +That prerequisite is now complete. Do not execute this older lifecycle-only packet; +use the [combined release review](../release/content-acceptance-review-2026-10-31.md). +The order is: existing historical custody repairfa2763d1 and recurring route4d848106, +then inactive source installation8f442bf0, then this image release, then native +recurring-backup proof and schedule activation. The installer pins the current +snapshot helper; deploying this image first would invalidate its reviewed preimage. +The image controller enforces the completed installer receipt and inactive/disabled +backup units before creating an execution journal. Do not bypass that dependency. + +After the existing prerequisites have their independently verified returns, run +this read-only check from the workstation: + +```bash +python3 /home/worsch/railiance-apps/tools/coulomb-image-release.py \ + --plan /home/worsch/railiance-apps/release/coulomb-content-lifecycle-f306dbf.json \ + --preflight \ + --expect-digest sha256:c9c3231620721ef3ea9b2bf22fc6c62fe3ed8efb91386cf7ec96acc6959dae08 +``` + +Only a `live_release_preflight_verified` return at that exact digest makes the +packet ready for attended execution. A changed baseline needs a revised review. +For that historical packet, the founder execution command replaced `--preflight` with `--execute`; its +terminal prompt requires `DEPLOY` followed by that full digest. The supervision +rule is recorded in the-custodian/docs/agent-autonomy-decision.md: “The supervisor +approves that proposal or performs it through the privileged execution path.” +This release has not been executed or approved through the retention-policy answer. + +Execution creates a fresh immutable paired primary backup/readback before Helm, +checks the current complete data and migration fingerprints, performs an atomic +image-only upgrade, verifies owner/outsider/public endpoints and refreshes the +snapshot image pin. A failure restores revision19 and the recognized previous pin +without restoring old data or downgrading schema. Inspect the durable receipt +before any retry. Expected receipt status is `image_candidate_deployed_verified` +under `/var/lib/coulomb-social-backup/releases/c9c3231620721ef3ea9b2bf22fc6c62fe3ed8efb91386cf7ec96acc6959dae08/receipt.json`. + +The current observation binds93business rows/86files. The retained07:13 recovery +point has92business rows: the differences are sessions and login timestamps, with +identical content and migration history. Candidate/rollback rehearsals preserve +all13tables/86files, verify85export files and40owner/40outsider views on that point. +They do not establish recovery of the newer session; independent retrieval/restore +of the fresh execution point remains part of the release return. + +There are no new infrastructure reservations. The action adds one encrypted +primary backup object and temporary local scratch under the admitted capacity +bounds. Incremental provider cost is unpriced; no EUR autopilot budget is claimed. +Application writes pause during paired capture and the Recreate rollout. Evidence: +`../release/content-lifecycle-release-preparation-2026-10-31.json`, its proposal, +native target/preflight/row-drift receipts, and the candidate recovery receipt. + + +### October31 Visual preservation follow-through + +A new private `~/.config/coulomb-social/auth/exports/visual-bundle-20261031` retains +242Visual references as90distinct local images and populates236draft article +Visuals plus67space indexes. The original rendering cards and source capture are +unchanged. Offline image decoding and exact body/date preservation pass; neither +proves appearance fidelity to Bubble. Use these retained local assets for the +source/product comparison after each mapping is ready. Graph exceptions, grants, +18body links and one attachment flag remain explicit. No productive import or +user verdict is inferred. Evidence: ../release/bubble-visual-preparation-2026-10-31.json. + +### October31 body-resource follow-through + +The later offline check verifies exact destination preservation for all 18 body-link +entries and 28 occurrences through the product renderer. It does not contact those +destinations or accept original appearance. Four deferred scrap relation fields +are now explicitly unresolved alongside the separate attachment flag; they were +present in raw source but omitted from the prior review flags. They must be +captured or given an explicit disposition before migration can be accepted. +Private supplemental report: `~/.config/coulomb-social/auth/exports/body-resources-20261031.json`. +Public proof: `../release/bubble-body-resources-2026-10-31.json`. + + +### Confirmed attachment modal and recovered files + +The user clarified that Bubble’s Add attachment image opens the modal used for +both adding and downloading files. The bounded read used only that opener and +Open file. Two files are retained, with exact matches between UI downloads and +independent CDN fetches. Original file-child creation/modification dates and +relations are retained; the existing matched draft includes its attachment and +provenance without changing its body or existing dates. The example outside the +original capture remains separately retained for reconciliation. + +Private review/downloads: +`~/.config/coulomb-social/auth/exports/attachment-bundle-20261031/attachments-index.html`. +The four separate scrap relations remain unresolved. Empty attachment modals do +not establish absence of scraps. No production import or productive acceptance +is inferred. Evidence: ../release/bubble-attachment-recovery-2026-10-31.json. + + +The four unresolved scrap cases now have a focused private review: +`~/.config/coulomb-social/auth/exports/scrap-review-20261031/index.html`. +Their navigator and attachment-modal reads do not expose scrap records. Keep +raw references and existing page bodies/dates until original evidence or an +explicit per-case disposition arrives (decision6db14274-4450-4ae2-b77a-cd4c88369eaa). + + +### Accepted disposition of the four scrap cases + +Bernd explicitly confirmed that all four reviewed scrap cases are obsolete legacy +relicts and no longer needed. Decision6db14274-4450-4ae2-b77a-cd4c88369eaa is resolved: +omit those four scrap references from migration/content acceptance requirements. +No further scrap capture or user verdict is needed. Their original raw evidence, +page bodies and source dates remain retained; recovered attachments remain in scope. +The preceding unresolved-scrap notes describe the earlier investigation. This +specific disposition does not waive other source exceptions or productive review. +Evidence: ../release/bubble-scrap-disposition-2026-10-31.json. diff --git a/registry/release-units/coulomb-social.yaml b/registry/release-units/coulomb-social.yaml index 22fabde..e88566b 100644 --- a/registry/release-units/coulomb-social.yaml +++ b/registry/release-units/coulomb-social.yaml @@ -35,7 +35,7 @@ pins: digest: hashAlgorithm: SHA-256 normalisationAlgorithm: ociArtifactDigest/v1 - value: sha256:522bcd7fce92436765a60c1e4afa2598f3cc40913352153ff1c4319a99ca92ed + value: sha256:b58d858abd5b4acbd6b893c3f622f7a9fdb5e606164ab9866eb3cfaa04df1a0e sources: - name: coulomb-social type: git @@ -43,4 +43,4 @@ pins: digest: hashAlgorithm: SHA-1 normalisationAlgorithm: gitCommit/v1 - value: 23ffe2cf4b214666754a2420558943438e54251a + value: 31eaa581a65e454b6ef9979326c5bdfe10e13806 diff --git a/scripts/audit-bubble-access.py b/scripts/audit-bubble-access.py new file mode 100644 index 0000000..2c93065 --- /dev/null +++ b/scripts/audit-bubble-access.py @@ -0,0 +1,190 @@ +#!/usr/bin/env python3 +"""Prepare an offline private access inventory; never infer target identities or grants.""" + +import argparse +import hashlib +import html +import json +import os +import re +import stat +import sys +from collections import Counter +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) +from coulomb_social.apps.spaces.bubble_transfer import normalize_ref # noqa: E402 + +FIELDS = { + 'spaces': {'Created By': False, 'crew_list_user': True, + 'members_list_user': True, 'guests_list_user': True}, + 'articles': {'Created By': False, '_creator_user': False, + '_owner_user': False, 'moders_list_user': True}, +} +OPTIONS = {'spaces': ['isprivate_boolean', 'addnewusersasmember_boolean', + 'addnewusersasguest_boolean'], + 'articles': ['access_option_visibility']} + + +def sha(raw): + return hashlib.sha256(raw).hexdigest() + + +def private_read(path): + if any(p.is_symlink() for p in (path, *path.parents)): + raise ValueError('source symlink') + info = path.stat() + if (not stat.S_ISREG(info.st_mode) or info.st_uid != os.getuid() + or info.st_mode & 0o077 or info.st_nlink != 1 or info.st_size > 16 * 1024**2): + raise ValueError('private bounded regular source required') + return path.read_bytes() + + +def reference(value): + try: + result = normalize_ref(value) + except (ValueError, TypeError): + return None + return result if re.fullmatch(r'[0-9]+x[0-9]+', result) else None + + +def field_state(row, field, many): + if field not in row: + return {'state': 'missing', 'principals': []} + raw = row[field] + if raw is None: + return {'state': 'null', 'raw': raw, 'principals': []} + if many and not isinstance(raw, list): + return {'state': 'invalid_container', 'raw': raw, 'principals': []} + if not many and not isinstance(raw, str): + return {'state': 'invalid_container', 'raw': raw, 'principals': []} + values = raw if many else [raw] + refs = [reference(v) for v in values] + return {'state': ('empty' if not values else 'unresolved_reference' + if None in refs else 'observed'), + 'raw': raw, 'principals': sorted(set(r for r in refs if r is not None)), + 'unresolved_count': sum(r is None for r in refs)} + + +def audit(inputs): + """Source values remain private; the separate summary contains aggregate counts only.""" + records = {} + principals = set() + summary = {} + for kind in FIELDS: + source = inputs[kind] + if not isinstance(source, dict) or any(not isinstance(v, dict) for v in source.values()): + raise ValueError('record dictionaries required') + records[kind] = [] + counters = {f: Counter() for f in FIELDS[kind]} + for key, row in sorted(source.items()): + fields = {f: field_state(row, f, many) for f, many in FIELDS[kind].items()} + for f, value in fields.items(): + counters[f][value['state']] += 1 + principals.update(value['principals']) + records[kind].append({'source_id': key, 'fields': fields, + 'options': {f: {'present': f in row, 'value': row.get(f)} + for f in OPTIONS[kind]}}) + summary[kind] = { + 'records': len(source), 'field_states': counters, + 'option_fields': {f: { + 'present': sum(f in row for row in source.values()), + 'missing': sum(f not in row for row in source.values()), + 'distinct_observed_values': len({json.dumps(row[f], sort_keys=True) + for row in source.values() if f in row}), + } for f in OPTIONS[kind]}, + } + retained = inputs['retained_spaces'] + if not isinstance(retained, dict): + raise ValueError('retained inventory required') + known = set(retained) | set(inputs['spaces']) + summary.update({'known_spaces': len(known), 'fresh_space_records': len(inputs['spaces']), + 'inventory_only_spaces': len(set(retained) - set(inputs['spaces'])), + 'distinct_source_principals': len(principals), + 'target_identity_mappings_established': 0, + 'target_grants_proposed': 0, 'production_changed': False, + 'source_visibility_semantics_verified': False}) + return {'schema': 'coulomb.bubble-access-inventory.v1', 'summary': summary, + 'source_principals': sorted(principals), 'records': records, + 'inventory_only_space_ids': sorted(set(retained) - set(inputs['spaces'])), + 'limitations': [ + 'Missing fields are unknown, not empty grants.', + 'Source roles and visibility are observed values, not target permissions.', + 'Existing one-space owner admission does not map every source identity.', + 'This capture does not prove complete or current provider membership.', + 'No target identity, role mapping or import manifest is created.']} + + +def review_html(report): + rows = [] + for kind, records in report['records'].items(): + for row in records: + data = json.dumps(row, ensure_ascii=False, indent=2) + rows.append('
' + html.escape(kind + ': ' + row['source_id']) + + '
' + html.escape(data) + '
') + return ('' + '' + 'Private source access inventory' + '

Private source access inventory

Observed source metadata only. ' + 'No target identities or grants have been inferred.

'
+            + html.escape(json.dumps(report['summary'], indent=2)) + '
    ' + + ''.join('
  • ' + html.escape(x) + '
  • ' for x in report['limitations']) + + '
' + ''.join(rows) + '').encode() + + +def prepare(source, out): + raw = {kind: private_read(source / 'source-capture' / (kind + '.json')) + for kind in [*FIELDS, 'retained_spaces']} + receipt_raw = private_read(source / 'reconciliation.json') + receipt = json.loads(receipt_raw) + if (receipt['schema'] != 'coulomb.bubble-review-bundle.v1' + or receipt['status'] != 'completed_offline_preparation'): + raise ValueError('completed review bundle required') + hashes = {k: sha(v) for k, v in raw.items()} + if any(receipt['source_sha256'].get(k) != v for k, v in hashes.items()): + raise ValueError('source digest mismatch') + report = audit({k: json.loads(v) for k, v in raw.items()}) + report['source_sha256'] = hashes + report['source_report_sha256'] = sha(receipt_raw) + if out.exists() or any(p.is_symlink() for p in (out, *out.parents)): + raise ValueError('new private output required') + parent = out.parent.stat() + if (parent.st_uid != os.getuid() or parent.st_mode & 0o077 + or out.resolve().is_relative_to(Path(__file__).resolve().parents[1]) + or out.resolve().is_relative_to(source.resolve())): + raise ValueError('private output outside repository and source required') + for kind, data in raw.items(): + if private_read(source / 'source-capture' / (kind + '.json')) != data: + raise ValueError('source changed during audit') + if private_read(source / 'reconciliation.json') != receipt_raw: + raise ValueError('source report changed during audit') + out.mkdir(mode=0o700) + outputs = {'access-inventory.json': json.dumps(report, indent=2).encode(), + 'index.html': review_html(report)} + for name, data in outputs.items(): + with os.fdopen(os.open(out / name, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600), 'wb') as f: + f.write(data) + f.flush() + os.fsync(f.fileno()) + return {'schema': 'coulomb.bubble-access-audit-result.v1', **report['summary'], + 'source_sha256': hashes, 'source_report_sha256': sha(receipt_raw), + 'output_sha256': {k: sha(v) for k, v in outputs.items()}} + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('source', type=Path) + parser.add_argument('output', type=Path) + args = parser.parse_args() + try: + result = prepare(args.source, args.output) + except Exception: + print('Access audit refused; no identity or grant inferred.', file=sys.stderr) + return 1 + print(json.dumps(result)) + return 0 + + +if __name__ == '__main__': + raise SystemExit(main()) diff --git a/scripts/bubble-resource-utils.mjs b/scripts/bubble-resource-utils.mjs new file mode 100644 index 0000000..63fc0bd --- /dev/null +++ b/scripts/bubble-resource-utils.mjs @@ -0,0 +1,78 @@ +/** Select typed content resources; never retain user/session envelopes or raw responses. */ +import {normalizeBubbleRef} from './bubble-corpus-utils.mjs'; + +export const RESOURCE_TYPES = new Set(['custom.scrap', 'custom.relattachment', 'custom.attachment']); +const validId = value => typeof value === 'string' && /^[0-9]+x[0-9]+$/.test(value); + +/** Bind observed attachment relations to child articles and their original files. */ +export function attachmentRows(parent, resources, articles) { + const rows=[]; + for (const [key,versions] of Object.entries(resources.records)) { + if (!key.startsWith('custom.relattachment:')) continue; + if (!versions.some(r=>normalizeBubbleRef(r.to_custom_article)===parent)) continue; + if (versions.length!==1) throw new Error('attachment_relation_drift'); + const relation=versions[0], id=normalizeBubbleRef(relation._chunk_custom_article); + const child=articles[id]; + if (!validId(id)||!child||normalizeBubbleRef(child.context_custom_article)!==parent) + throw new Error('attachment_child_binding'); + const source=child.logo_image; + if (typeof source!=='string') throw new Error('attachment_file_missing'); + const url=new URL(source.startsWith('//')?'https:'+source:source); + if (url.protocol!=='https:'||url.username||url.password||url.port||url.hash|| + !/^[a-f0-9]{32}\.cdn\.bubble\.io$/.test(url.hostname)) + throw new Error('attachment_file_origin'); + rows.push({relation_id:normalizeBubbleRef(relation._id),article_id:id, + url:url.href,name:child.nomer_text||'',created_at:child['Created Date'], + modified_at:child['Modified Date']}); + } + if (rows.length>100) throw new Error('attachment_count_bound'); + return rows; +} + +export function resourceParents(articles) { + return Object.entries(articles).filter(([,a]) => a.hasattachments_boolean === true || + (Object.hasOwn(a, '_list_custom_scrap') && + !(Array.isArray(a._list_custom_scrap) && a._list_custom_scrap.length === 0))) + .map(([id]) => normalizeBubbleRef(id)).filter(validId).sort(); +} + +export function ingestResourceBody(text, output) { + const visit = obj => { + if (Array.isArray(obj)) { obj.forEach(visit); return; } + if (!obj || typeof obj !== 'object') return; + // Typed initialization is a terminal envelope. Never walk into unrelated + // user/session data, even if it contains something resembling a resource. + if (typeof obj.type === 'string' && Object.hasOwn(obj, 'data')) { + const id = normalizeBubbleRef(obj.id), data = obj.data; + if (!RESOURCE_TYPES.has(obj.type) || !validId(id)) return; + if (data && typeof data === 'object' && !Array.isArray(data) && + Object.keys(data).length === 0) { + output.empty[`${obj.type}:${id}`] = {type:obj.type,id}; + } else if (data && data._type === obj.type && normalizeBubbleRef(data._id) === id) { + visit({_type:obj.type,_id:id,_source:data}); + } + return; + } + if (RESOURCE_TYPES.has(obj._type)) { + const id = normalizeBubbleRef(obj._id), data = obj._source; + if (validId(id) && data && typeof data === 'object' && !Array.isArray(data) && + (!data._id || normalizeBubbleRef(data._id) === id) && + (!data._type || data._type === obj._type)) { + const key = `${obj._type}:${id}`; + // Keep source observations, not a guessed union of different versions. + const observed = {_type:obj._type,_id:id,...data}; + const versions = output.records[key] ||= []; + if (!versions.some(v => JSON.stringify(v) === JSON.stringify(observed))) versions.push(observed); + } + return; + } + if (Array.isArray(obj.responses)) obj.responses.forEach(visit); + if (obj.hits && Array.isArray(obj.hits.hits)) obj.hits.hits.forEach(visit); + }; + try { visit(JSON.parse(text)); } + catch { + for (const line of text.split('\n')) { + try { visit(JSON.parse(line)); } catch { /* Not a JSON record. */ } + } + } +} diff --git a/scripts/capture-bubble-attachments.mjs b/scripts/capture-bubble-attachments.mjs new file mode 100644 index 0000000..1ae0560 --- /dev/null +++ b/scripts/capture-bubble-attachments.mjs @@ -0,0 +1,91 @@ +#!/usr/bin/env node +/** Recover existing files through the user-confirmed Bubble attachment modal. */ +import {chromium} from 'playwright'; +import {readFileSync,mkdirSync,writeFileSync,lstatSync,openSync,writeSync,fsyncSync, + closeSync,renameSync,unlinkSync} from 'node:fs'; +import {resolve,dirname,join} from 'node:path'; +import {homedir} from 'node:os'; +import {fileURLToPath} from 'node:url'; +import {createHash} from 'node:crypto'; +import {parseArgs} from 'node:util'; +import {assertSessionFresh,isCorpusResponse,boundedCaptureBody,ingestCapturedBody, + normalizeBubbleRef} from './bubble-corpus-utils.mjs'; +import {resourceParents,ingestResourceBody,attachmentRows} from './bubble-resource-utils.mjs'; +const ROOT=resolve(dirname(fileURLToPath(import.meta.url)),'..'),BASE='https://coulomb.social'; +const MAX_FILE=64*1024*1024,MAX_TOTAL=512*1024*1024; +const sha=raw=>createHash('sha256').update(raw).digest('hex'); +const {values}=parseArgs({options:{articles:{type:'string'},output:{type:'string'}, + 'expect-articles-sha256':{type:'string'},'example-article':{type:'string'}}}); +function noLinks(path){for(let p=path;;p=dirname(p)){if(lstatSync(p).isSymbolicLink())throw Error('symlink');if(dirname(p)===p)break;}} +function privateRead(path){noLinks(path);const s=lstatSync(path);if(!s.isFile()||s.uid!==process.getuid()||(s.mode&0o077)||s.nlink!==1||s.size>10000000)throw Error('private_input');return readFileSync(path);} +let browser,total=0; +try{ + if(!values.articles||!values.output||!/^[a-f0-9]{64}$/.test(values['expect-articles-sha256']||''))throw Error('source_binding'); + const source=resolve(values.articles),out=resolve(values.output),raw=privateRead(source); + if(sha(raw)!==values['expect-articles-sha256'])throw Error('source_drift'); + const example=values['example-article'];if(example&&!/^\d+x\d+$/.test(example))throw Error('example_id'); + const parents=[...new Set([...(example?[example]:[]),...resourceParents(JSON.parse(raw))])]; + if(!parents.length||parents.length>10)throw Error('target_bound'); + if(out===ROOT||out.startsWith(ROOT+'/')||out.startsWith(dirname(source)+'/'))throw Error('output_overlap'); + noLinks(dirname(out));const parent=lstatSync(dirname(out));if(parent.uid!==process.getuid()||(parent.mode&0o077))throw Error('private_parent'); + const state=JSON.parse(privateRead(join(homedir(),'.config/coulomb-social/auth/storage-state.json')));assertSessionFresh(state); + mkdirSync(out,{mode:0o700});mkdirSync(join(out,'files'),{mode:0o700}); + const result={schema:'coulomb.bubble-attachment-modal-capture.v1',status:'capturing', + started_at:new Date().toISOString(),source_articles_sha256:sha(raw),target_article_ids:parents, + resources:{records:{},empty:{}},articles:{},attempts:[],files:{}, + resource_completeness_verified:false,source_mutations:false,production_imported:false}; + function checkpoint(){const f=join(out,'capture.json.partial'),fd=openSync(f,'w',0o600);try{writeFileSync(fd,JSON.stringify(result,null,2)+'\n');fsyncSync(fd);}finally{closeSync(fd);}renameSync(f,join(out,'capture.json'));const d=openSync(out,'r');try{fsyncSync(d);}finally{closeSync(d);}} + checkpoint(); + browser=await chromium.launch({headless:true});const context=await browser.newContext({storageState:state,acceptDownloads:true}); + const page=await context.newPage(),pending=new Set(),observedArticles={},observedResources={records:{},empty:{}}; + let responseFailures=0; + const observer=r=>{if(!isCorpusResponse(r.url(),BASE))return;const p=(async()=>{const b=await boundedCaptureBody(r);if(b.text&&r.ok()){ingestResourceBody(b.text,observedResources);ingestCapturedBody(b.text,{},observedArticles);}else responseFailures++;})();pending.add(p);p.finally(()=>pending.delete(p));}; + page.on('response',observer); + async function retain(url){ + if(result.files[url])return result.files[url]; + const response=await fetch(url,{redirect:'error',signal:AbortSignal.timeout(90000)}); + if(!response.ok||!response.body||Number(response.headers.get('content-length'))>MAX_FILE)throw Error('download_refused'); + const path='files/'+sha(Buffer.from(url)),fd=openSync(join(out,path),'wx',0o600),hash=createHash('sha256');let size=0; + try{for await(const chunk of response.body){size+=chunk.length;total+=chunk.length;if(size>MAX_FILE||total>MAX_TOTAL)throw Error('download_bound');hash.update(chunk);let offset=0;while(offsetdownloads.push(d); + const onPage=p=>p.on('download',onDownload);page.on('download',onDownload);context.on('page',onPage); + try{ + await buttons.nth(i).click(); + for(let n=0;n<60&&!downloads.length;n++)await page.waitForTimeout(100); + if(downloads.length!==1)throw Error('single_ui_download_required'); + const download=downloads[0],url=download.url();const matches=rows.filter(r=>r.url===url); + if(matches.length!==1)throw Error('ui_source_binding'); + const row=matches[0],file=await retain(url); + const received=await download.path(); + if(!received||lstatSync(received).size!==file.bytes||sha(readFileSync(received))!==file.sha256) + throw Error('independent_download_byte_mismatch'); + attempt.rows.push({...row,...file,suggested_filename:download.suggestedFilename(), + ui_download_url_verified:true,ui_and_independent_download_bytes_match:true}); + }finally{page.off('download',onDownload);context.off('page',onPage);for(const p of context.pages())if(p!==page)await p.close();} + } + if(new Set(attempt.rows.map(r=>r.relation_id)).size!==rows.length)throw Error('duplicate_or_missing_relation'); + for(const row of rows){const key='custom.relattachment:'+row.relation_id;result.resources.records[key]=observedResources.records[key];result.articles[row.article_id]=observedArticles[row.article_id];} + if(!observedArticles[id])throw Error('parent_source_missing');result.articles[id]=observedArticles[id]; + result.attempts.push(attempt);checkpoint(); + console.log(JSON.stringify({visited:result.attempts.length,modal_files:count,retained_files:Object.keys(result.files).length,retained_bytes:total})); + } + page.off('response',observer);await Promise.allSettled([...pending]); + await browser.close();browser=null; + if(sha(privateRead(source))!==result.source_articles_sha256)throw Error('source_changed'); + result.status='completed_bounded_attachment_modal_capture';result.completed_at=new Date().toISOString();checkpoint(); + console.log(JSON.stringify({status:result.status,visited:result.attempts.length,relations:Object.keys(result.resources.records).length,files:Object.keys(result.files).length,bytes:total,private_capture_sha256:sha(readFileSync(join(out,'capture.json'))),source_mutations:false,production_imported:false,resource_completeness_verified:false})); +}catch(error){if(browser)await browser.close();console.error('Attachment modal capture stopped: '+(error.message||'unknown').replace(/[^a-zA-Z0-9_]/g,'_').slice(0,80));process.exitCode=1;} diff --git a/scripts/capture-bubble-resources.mjs b/scripts/capture-bubble-resources.mjs new file mode 100644 index 0000000..152acef --- /dev/null +++ b/scripts/capture-bubble-resources.mjs @@ -0,0 +1,119 @@ +#!/usr/bin/env node +/** Bounded read-only page visits for retained scrap/attachment source gaps. */ +import {chromium} from 'playwright'; +import {readFileSync, mkdirSync, openSync, writeFileSync, fsyncSync, closeSync, + renameSync, lstatSync} from 'node:fs'; +import {resolve, dirname, join} from 'node:path'; +import {homedir} from 'node:os'; +import {fileURLToPath} from 'node:url'; +import {createHash} from 'node:crypto'; +import {parseArgs} from 'node:util'; +import {assertSessionFresh, boundedCaptureBody, isCorpusResponse, + ingestCapturedBody} from './bubble-corpus-utils.mjs'; +import {resourceParents, ingestResourceBody} from './bubble-resource-utils.mjs'; + +const ROOT=resolve(dirname(fileURLToPath(import.meta.url)),'..'); +const BASE='https://coulomb.social'; +const sha=raw=>createHash('sha256').update(raw).digest('hex'); +const {values}=parseArgs({options:{articles:{type:'string'},output:{type:'string'}, + 'expect-articles-sha256':{type:'string'}}}); +let browser; +function noSymlinks(path) { + for(let p=path;;p=dirname(p)) { + if(lstatSync(p).isSymbolicLink()) throw new Error('symlink_refused'); + if(dirname(p)===p) break; + } +} +function privateRead(path) { + noSymlinks(path); + const info=lstatSync(path); + if(!info.isFile() || info.uid!==process.getuid() || (info.mode&0o077) || + info.nlink!==1 || info.size>10_000_000) throw new Error('private_input_required'); + return readFileSync(path); +} +try { + if(!values.articles || !values.output || !/^[a-f0-9]{64}$/.test( + values['expect-articles-sha256']||'')) throw new Error('exact_source_required'); + const input=resolve(values.articles),out=resolve(values.output); + const raw=privateRead(input); + if(sha(raw)!==values['expect-articles-sha256']) throw new Error('source_drift'); + const targets=resourceParents(JSON.parse(raw)); + if(!targets.length || targets.length>10) throw new Error('bounded_targets_required'); + if(out===ROOT || out.startsWith(ROOT+'/') || out.startsWith(dirname(input)+'/')) + throw new Error('output_overlap'); + noSymlinks(dirname(out)); + const parent=lstatSync(dirname(out)); + if(parent.uid!==process.getuid() || (parent.mode&0o077)) throw new Error('private_parent_required'); + const state=JSON.parse(privateRead(join(homedir(),'.config/coulomb-social/auth/storage-state.json'))); + assertSessionFresh(state); + const result={schema:'coulomb.bubble-resource-capture.v1',status:'capturing', + started_at:new Date().toISOString(),source_articles_sha256:sha(raw),target_article_ids:targets, + attempts:[],resources:{records:{},empty:{}},parent_articles:{}, + response_status_counts:{},response_timeouts:0,response_failures:0, + production_imported:false,resource_completeness_verified:false,ui_controls_activated:false}; + mkdirSync(out,{mode:0o700}); + function checkpoint() { + const file=join(out,'capture.json.partial'); + const fd=openSync(file,'w',0o600); + try {writeFileSync(fd,JSON.stringify(result,null,2)+'\n');fsyncSync(fd);} finally {closeSync(fd);} + renameSync(file,join(out,'capture.json')); + const directory=openSync(out,'r');try {fsyncSync(directory);} finally {closeSync(directory);} + } + checkpoint(); + browser=await chromium.launch({headless:true}); + const context=await browser.newContext({storageState:state}); + const page=await context.newPage(); + const pending=new Set(); + const observer=response=>{ + if(!isCorpusResponse(response.url(),BASE)) return; + const operation=(async()=>{ + const status=String(response.status()); + result.response_status_counts[status]=(result.response_status_counts[status]||0)+1; + const body=await boundedCaptureBody(response); + if(body.status==='timeout') result.response_timeouts++; + else if(body.status==='failed') result.response_failures++; + else if(body.text.length<5_000_000 && response.ok()) { + ingestResourceBody(body.text,result.resources); + const spaces={},articles={};ingestCapturedBody(body.text,spaces,articles); + for(const id of targets) if(articles[id]) result.parent_articles[id]=articles[id]; + } + checkpoint(); + })(); + pending.add(operation);operation.finally(()=>pending.delete(operation)); + }; + page.on('response',observer); + for(const id of targets) { + let status='visited',routeMatched=false,resourceControlCandidates=0; + try { + await page.goto(BASE+'/pg_chunk/'+id,{waitUntil:'domcontentloaded',timeout:20000}); + await page.waitForTimeout(2000); + routeMatched=page.url().split(/[?#]/)[0]===BASE+'/pg_chunk/'+id; + // Count only exact resource labels; never print titles, bodies or account names. + resourceControlCandidates=await page.getByRole('button',{ + name:/^(attachments?|scraps?|anhänge|anhang)$/i}).count(); + } catch {status='navigation_failed';} + await Promise.allSettled([...pending]); + result.attempts.push({id,status,route_matched:routeMatched, + resource_control_candidates:resourceControlCandidates});checkpoint(); + console.log(JSON.stringify({visits:result.attempts.length, + typed_resources:Object.keys(result.resources.records).length})); + } + page.off('response',observer);await Promise.allSettled([...pending]); + await browser.close();browser=null; + if(sha(privateRead(input))!==result.source_articles_sha256) throw new Error('source_changed'); + result.status='completed_bounded_observation';result.completed_at=new Date().toISOString(); + checkpoint(); + console.log(JSON.stringify({status:result.status,visits:result.attempts.length, + typed_resources:Object.keys(result.resources.records).length, + empty_resource_envelopes:Object.keys(result.resources.empty).length, + observed_parent_articles:Object.keys(result.parent_articles).length, + response_timeouts:result.response_timeouts,response_failures:result.response_failures, + resource_control_candidates:result.attempts.reduce((n,a)=>n+a.resource_control_candidates,0), + source_articles_sha256:result.source_articles_sha256, + private_capture_sha256:sha(readFileSync(join(out,'capture.json'))), + resource_completeness_verified:false,production_imported:false})); +} catch { + if(browser) await browser.close(); + console.error('Resource capture refused or stopped; private checkpoint retained, no completeness inferred.'); + process.exitCode=1; +} diff --git a/scripts/capture-bubble-visuals.py b/scripts/capture-bubble-visuals.py new file mode 100644 index 0000000..170ef44 --- /dev/null +++ b/scripts/capture-bubble-visuals.py @@ -0,0 +1,277 @@ +#!/usr/bin/env python3 +"""Capture retained Bubble Visual URLs and prepare a private, hash-bound review bundle.""" + +import argparse +import copy +import hashlib +import json +import os +import re +import stat +import sys +import urllib.request +from datetime import UTC, datetime +from pathlib import Path +from urllib.parse import urlsplit + +CDN = '13a55be71f3a6a4c9ea356833df823c4.cdn.bubble.io' +MAX_ASSET = 16 * 1024 * 1024 +MAX_TOTAL = 256 * 1024 * 1024 +ROOT = Path(__file__).resolve().parents[1] + + +class Refused(Exception): + pass + + +def sha(data): + return hashlib.sha256(data).hexdigest() + + +def canonical_url(value): + if not isinstance(value, str) or any(ord(c) <= 32 for c in value): + raise Refused('invalid_visual_url') + value = 'https:' + value if value.startswith('//') else value + try: + url = urlsplit(value) + valid = (url.scheme == 'https' and url.hostname == CDN and url.port in (None, 443) + and url.username is None and url.password is None and not url.query + and not url.fragment and url.path.startswith('/') and '\\' not in value) + except ValueError: + valid = False + if not valid: + raise Refused('outside_reviewed_visual_origin') + return value + + +class NoRedirect(urllib.request.HTTPRedirectHandler): + def redirect_request(self, req, fp, code, msg, headers, newurl): + raise Refused('visual_redirect_refused') + + +def image_extension(raw, media_type): + if raw.startswith(b'\x89PNG\r\n\x1a\n') and media_type == 'image/png': + return 'png' + if raw.startswith(b'\xff\xd8\xff') and media_type == 'image/jpeg': + return 'jpg' + if (len(raw) >= 12 and raw[:4] == b'RIFF' and raw[8:12] == b'WEBP' + and media_type == 'image/webp'): + return 'webp' + raise Refused('unsupported_visual_bytes_or_type') + + +def fetch(url): + url = canonical_url(url) + opener = urllib.request.build_opener(urllib.request.ProxyHandler({}), NoRedirect()) + request = urllib.request.Request(url, headers={'User-Agent': 'coulomb-visual-capture/1.0'}) + with opener.open(request, timeout=20) as response: + if response.status != 200 or response.geturl() != url: + raise Refused('unexpected_visual_response') + length = response.headers.get('Content-Length') + if length and (not length.isdigit() or int(length) > MAX_ASSET): + raise Refused('visual_size_limit') + raw = response.read(MAX_ASSET + 1) + if len(raw) > MAX_ASSET: + raise Refused('visual_size_limit') + if length and int(length) != len(raw): + raise Refused('incomplete_visual_response') + extension = image_extension(raw, response.headers.get_content_type()) + return raw, extension + + +def read_private(root, name, expected=None): + relative = Path(name) + if relative.is_absolute() or '..' in relative.parts or not relative.parts: + raise Refused('unsafe_bundle_path') + path = root / relative + if any(p.is_symlink() for p in (path, *path.parents)): + raise Refused('bundle_symlink_refused') + info = path.stat() + if (not stat.S_ISREG(info.st_mode) or info.st_uid != os.getuid() + or info.st_mode & 0o077 or info.st_nlink != 1 or info.st_size > MAX_ASSET): + raise Refused('private_bounded_source_required') + raw = path.read_bytes() + if expected is not None and sha(raw) != expected: + raise Refused('source_file_changed') + return raw + + +def publish(root, name, raw): + relative = Path(name) + if relative.is_absolute() or '..' in relative.parts or not relative.parts: + raise Refused('unsafe_output_path') + path = root / relative + if any(p.is_symlink() for p in (path, *path.parents)): + raise Refused('output_symlink_refused') + parent = root + for part in relative.parts[:-1]: + parent = parent / part + parent.mkdir(mode=0o700, exist_ok=True) + info = parent.stat() + if info.st_uid != os.getuid() or info.st_mode & 0o077: + raise Refused('private_output_directory_required') + fd = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW, 0o600) + with os.fdopen(fd, 'wb') as stream: + stream.write(raw) + stream.flush() + os.fsync(stream.fileno()) + if read_private(root, name) != raw: + raise Refused('capture_readback_failed') + + +def with_visual(raw, relative, *, index=False): + # Replace only the compiler's known empty field; never parse/reformat the body. + head, body = raw.split(b'\n---\n', 1) + if not head.startswith(b'---\n'): + raise Refused('draft_frontmatter_required') + new = b'visual: ' + json.dumps(relative).encode() + if index: + if re.search(rb'^visual:', head, re.M): + raise Refused('unexpected_index_visual') + head += b'\n' + new + else: + if len(re.findall(rb'^visual: ""$', head, re.M)) != 1: + raise Refused('empty_draft_visual_required') + head = re.sub(rb'^visual: ""$', lambda _: new, head, flags=re.M) + return head + b'\n---\n' + body + + +def capture(bundle, output, fetcher=fetch): + bundle = Path(os.path.abspath(bundle.expanduser())) + output = Path(os.path.abspath(output.expanduser())) + if (output.is_relative_to(ROOT) or output.is_relative_to(bundle) + or bundle.is_relative_to(output) + or any(p.is_symlink() for p in (output, *output.parents)) + or any((p / '.git').exists() for p in (output, *output.parents))): + raise Refused('private_separate_output_required') + parent = output.parent.stat() + if parent.st_uid != os.getuid() or parent.st_mode & 0o077: + raise Refused('private_owned_output_parent_required') + original = read_private(bundle, 'reconciliation.json') + report = json.loads(original) + if (report.get('schema') != 'coulomb.bubble-review-bundle.v1' + or report.get('status') != 'completed_offline_preparation'): + raise Refused('completed_original_bundle_required') + # Verify every declared file and the captured source bindings before any network read. + files = {name: read_private(bundle, name, digest) + for name, digest in report['file_sha256'].items()} + if any(not name.startswith(('source-bodies/', 'review-trees/')) for name in files): + raise Refused('unexpected_original_bundle_namespace') + for space in report['spaces']: + tree = space['tree'] + if space.get('import_ready') is not False: + raise Refused('unapproved_original_readiness') + if tree is None: + continue + if not re.fullmatch(r'review-trees/bubble-[0-9a-f]{24}', tree): + raise Refused('unexpected_review_tree_path') + for page in space['articles']: + if not re.fullmatch(re.escape(tree) + r'/pages/article-[0-9a-f]{24}\.md', + page['file']): + raise Refused('unexpected_review_page_path') + for name, digest in report['source_sha256'].items(): + files['source-capture/' + name + '.json'] = read_private( + bundle, 'source-capture/' + name + '.json', digest) + visuals = {} + for article in report['articles']: + refs = [ref['source'] for ref in article['references'] if ref['kind'] == 'visual'] + if len(refs) > 1: + raise Refused('ambiguous_visual_reference') + if refs: + visuals[article['id']] = canonical_url(refs[0]) + output.mkdir(mode=0o700) # Existing output always refuses; never replace a capture. + captured, total = {}, 0 + for url in sorted(set(visuals.values())): + raw, extension = fetcher(url) + if len(raw) > MAX_ASSET or total + len(raw) > MAX_TOTAL: + raise Refused('visual_capture_budget_exceeded') + total += len(raw) + name = 'captured-visuals/' + sha(raw) + '.' + extension + if name not in files: + files[name] = raw + captured[url] = {'file': name, 'sha256': sha(raw), 'bytes': len(raw)} + # Checkpoint captured bytes immediately; no final report means incomplete output. + if not (output / name).exists(): + publish(output, name, raw) + updated = copy.deepcopy(report) + by_id = {a['id']: a for a in updated['articles']} + materialized = 0 + for space in updated['spaces']: + tree = space['tree'] + if tree is None: + continue + for page in space['articles']: + url = visuals.get(page['id']) + if not url: + continue + visual = captured[url] + relative = 'assets/' + Path(visual['file']).name + files[tree + '/' + relative] = files[visual['file']] + files[page['file']] = with_visual(files[page['file']], relative) + page['file_sha256'] = sha(files[page['file']]) + materialized += 1 + if page['id'] == space['root_article_id']: + index = tree + '/pages/index.md' + files[index] = with_visual(files[index], relative, index=True) + # Keep all unresolved gates: retained bytes do not certify source/UI fidelity or grants. + space['import_ready'] = False + for identity, url in visuals.items(): + article = by_id[identity] + article['captured_visual'] = captured[url] + for ref in article['references']: + if ref['kind'] == 'visual': + ref['capture_status'] = 'bytes_retained_and_hashed' + ref['resolved'] = False + # Recheck the immutable inputs after fetching; source drift cannot publish completion. + if read_private(bundle, 'reconciliation.json') != original: + raise Refused('source_report_changed_during_capture') + for name, digest in report['file_sha256'].items(): + read_private(bundle, name, digest) + for name, digest in report['source_sha256'].items(): + read_private(bundle, 'source-capture/' + name + '.json', digest) + files['source-capture/reconciliation-before-visuals.json'] = original + for name, raw in sorted(files.items()): + if not name.startswith('captured-visuals/'): + publish(output, name, raw) + updated.update(status='completed_private_visual_preparation', + original_report_sha256=sha(original), + captured_at=datetime.now(UTC).isoformat(), + visual_capture={'references': len(visuals), 'unique_urls': len(captured), + 'downloaded_bytes': total, 'materialized_pages': materialized, + 'body_rewriting_performed': False, 'grants_changed': False, + 'browser_render_verified': False, 'production_imported': False}, + network_requests=len(captured), + file_sha256={name: sha(raw) for name, raw in sorted(files.items())}) + for directory in sorted([output, *(p for p in output.rglob('*') if p.is_dir())], + key=lambda p: len(p.parts), reverse=True): + fd = os.open(directory, os.O_RDONLY | os.O_DIRECTORY) + try: + os.fsync(fd) + finally: + os.close(fd) + raw_report = (json.dumps(updated, indent=2, sort_keys=True) + '\n').encode() + publish(output, 'reconciliation.json', raw_report) + fd = os.open(output, os.O_RDONLY | os.O_DIRECTORY) + try: + os.fsync(fd) + finally: + os.close(fd) + return updated['visual_capture'] + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('bundle', type=Path) + parser.add_argument('--output', type=Path, required=True) + args = parser.parse_args() + try: + print(json.dumps(capture(args.bundle, args.output))) + except Exception: # Private URLs, titles and provider errors never enter stdout/stderr. + print('Visual capture refused or incomplete; retain any partial output for review. ' + 'No final reconciliation.json means it is not a completed bundle.', file=sys.stderr) + return 1 + return 0 + + +if __name__ == '__main__': + raise SystemExit(main()) diff --git a/scripts/native_recovery/probe.py b/scripts/native_recovery/probe.py index 425a8b2..737e040 100644 --- a/scripts/native_recovery/probe.py +++ b/scripts/native_recovery/probe.py @@ -14,6 +14,7 @@ def run(v): assert len(Path('/proc/net/route').read_text().splitlines()) == 1 opener = urllib.request.build_opener(urllib.request.ProxyHandler({})) cookie = '' + target = v.get('target', 'alice') def call(port, path, data=None, *, token=None, auth=False): nonlocal cookie @@ -63,9 +64,22 @@ def run(v): admin = login('admin', v['admin_password']) assert admin, 'synthetic_admin_login' if v['mode'] == 'bootstrap': - for user in ['alice', 'svc-auth']: + for user in (['alice', 'svc-auth', 'bob', 'operator', 'appoperator', 'svc-other', + 'victim'] if v.get('authority_matrix') else ['alice', 'svc-auth']): graphql(admin, 'mutation($u:CreateUserInput!){createUser(user:$u){id}}', { 'u': {'id': user, 'email': user + '@example.test', 'displayName': user}}) + if v.get('authority_matrix'): + groups = graphql(admin, '{groups{id displayName}}')['groups'] + admin_id = next(g['id'] for g in groups if g['displayName'] == 'lldap_admin') + for user, group in [('operator', admin_id), ('svc-other', next( + g['id'] for g in groups if g['displayName'] == 'lldap_strict_readonly'))]: + graphql(admin, 'mutation($u:String!,$g:Int!){addUserToGroup(' + 'userId:$u,groupId:$g){ok}}', {'u': user, 'g': group}) + app_id = graphql(admin, 'mutation{createGroup(name:"application-admins"){id}}') + graphql(admin, 'mutation($u:String!,$g:Int!){addUserToGroup(' + 'userId:$u,groupId:$g){ok}}', + {'u': 'appoperator', 'g': app_id['createGroup']['id']}) + graphql(admin, 'mutation{createGroup(name:"victim-group"){id}}') return {'status': 'prepared', 'admin_token': admin} if v['mode'] == 'bind_role': groups = graphql(admin, '{groups{id displayName}}')['groups'] @@ -85,6 +99,54 @@ def run(v): 'query': 'mutation{createGroup(name:"unexpected-admin-rights"){id}}'}, token=service) assert code >= 400 or body.get('errors'), 'directory_group_creation_refused' return {'status': 'role_set', 'directory_group_creation_refused': True} + if v['mode'] == 'authority_context': + service = login('svc-auth', v['bind_password']) + assert service, 'service_directory_login' + return {'status': 'authority_context', 'service_token': service, + 'admin_token': admin} + if v['mode'] == 'directory_authentication': + return {'old_password_accepted': bool(login(v['target'], v['old_password'])), + 'new_password_accepted': bool(login(v['target'], v['new_password']))} + if v['mode'] == 'write_boundaries': + service = login('svc-auth', v['bind_password']) + assert service, 'service_directory_login' + # Positive controls distinguish an authorization refusal from invalid queries. + create_user = 'mutation($u:CreateUserInput!){createUser(user:$u){id}}' + update_user = 'mutation($u:UpdateUserInput!){updateUser(user:$u){ok}}' + delete_user = 'mutation($u:String!){deleteUser(userId:$u){ok}}' + create_group = 'mutation($n:String!){createGroup(name:$n){id}}' + delete_group = 'mutation($g:Int!){deleteGroup(groupId:$g){ok}}' + add = 'mutation($u:String!,$g:Int!){addUserToGroup(userId:$u,groupId:$g){ok}}' + remove = 'mutation($u:String!,$g:Int!){removeUserFromGroup(userId:$u,groupId:$g){ok}}' + graphql(admin, create_user, {'u': {'id': 'control', 'email': 'control@example.test'}}) + graphql(admin, update_user, {'u': {'id': 'control', 'email': 'changed@example.test'}}) + gid = graphql(admin, create_group, {'n': 'control-group'})['createGroup']['id'] + graphql(admin, add, {'u': 'control', 'g': gid}) + graphql(admin, remove, {'u': 'control', 'g': gid}) + graphql(admin, delete_group, {'g': gid}) + graphql(admin, delete_user, {'u': 'control'}) + snapshot_query = '{users{id email displayName groups{id}} groups{id displayName}}' + before = graphql(admin, snapshot_query) + groups = before['groups'] + admin_id = next(g['id'] for g in groups if g['displayName'] == 'lldap_admin') + victim_id = next(g['id'] for g in groups if g['displayName'] == 'victim-group') + mutations = [ + ('create_user', create_user, {'u': {'id': 'unexpected', 'email': 'x@example.test'}}), + ('edit_other_user', update_user, {'u': {'id': 'victim', 'email': 'x@example.test'}}), + ('delete_user', delete_user, {'u': 'victim'}), + ('create_group', create_group, {'n': 'unexpected'}), + ('delete_group', delete_group, {'g': victim_id}), + ('self_promote_admin', add, {'u': 'svc-auth', 'g': admin_id}), + ('remove_admin_membership', remove, {'u': 'operator', 'g': admin_id})] + denials = [] + for label, query, variables in mutations: + code, body = call(17170, '/api/graphql', {'query': query, 'variables': variables}, + token=service) + assert code >= 400 or body.get('errors'), 'unexpected_directory_write_' + label + denials.append(label) + assert graphql(admin, snapshot_query) == before, 'directory_metadata_changed' + return {'positive_controls': len(mutations), 'refused_mutations': denials, + 'directory_metadata_unchanged': True} ready(9091, '/api/health') if v['mode'] == 'reset_finish': code, body = call(9091, '/api/reset-password', {'password': v['new_password']}, auth=True) @@ -102,18 +164,18 @@ def run(v): 'native_identity_finish': True, 'reset_without_verified_challenge_refused': True, 'consumed_identity_token_replay_refused': True, - 'old_password_accepted_after': bool(login('alice', v['old_password'])), - 'new_password_accepted_after': bool(login('alice', v['new_password']))} - assert login('alice', v['old_password']), 'initial_password' - assert not login('alice', v['new_password']), 'replacement_not_initially_accepted' + 'old_password_accepted_after': bool(login(target, v['old_password'])), + 'new_password_accepted_after': bool(login(target, v['new_password']))} + assert login(target, v['old_password']), 'initial_password' + assert not login(target, v['new_password']), 'replacement_not_initially_accepted' code, body = call(9091, '/api/firstfactor', { 'username': 'svc-auth', 'password': v['bind_password'], 'keepMeLoggedIn': False}, auth=True) assert code != 200 or body.get('status') != 'OK', 'interactive_service_login_refused' code, body = call(9091, '/api/firstfactor', { - 'username': 'alice', 'password': v['old_password'], 'keepMeLoggedIn': False}, auth=True) + 'username': target, 'password': v['old_password'], 'keepMeLoggedIn': False}, auth=True) assert code == 200 and body.get('status') == 'OK', 'native_first_factor' cookie = '' - code, _ = call(9091, '/api/reset-password/identity/start', {'username': 'alice'}, auth=True) + code, _ = call(9091, '/api/reset-password/identity/start', {'username': target}, auth=True) assert code == 200, 'reset_start' return {'status': 'reset_requested', 'only_loopback_interface': True, 'external_routes_absent': True, 'old_password_initially_accepted': True, diff --git a/scripts/rehearse-import-inspection.py b/scripts/rehearse-import-inspection.py new file mode 100644 index 0000000..98e4d9e --- /dev/null +++ b/scripts/rehearse-import-inspection.py @@ -0,0 +1,162 @@ +"""Run synthetic corpus/receipt tests on disposable isolated PostgreSQL.""" + +import hashlib +import json +import subprocess +import sysconfig +import uuid +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +IMAGE = ( + "forgejo.coulomb.social/coulomb/coulomb-social@sha256:" + "522bcd7fce92436765a60c1e4afa2598f3cc40913352153ff1c4319a99ca92ed" +) +CODE = """import json,os,time +os.environ['DJANGO_SETTINGS_MODULE']='coulomb_social.settings.test' +from django.conf import settings +settings.DATABASES={'default':{'ENGINE':'django.db.backends.postgresql', + 'HOST':'inspection-db','NAME':'inspection','USER':'postgres','PORT':'5432'}} +import django +django.setup() +from django.db import connection +for attempt in range(30): + try: + connection.ensure_connection();break + except Exception:time.sleep(1) +else:raise RuntimeError('disposable_database_not_ready') +version=connection.pg_version +connection.close() +from coulomb_social.apps.spaces import import_inspection +original=import_inspection.observe_unit +checks=[] +def observe(*args): + with connection.cursor() as cursor: + cursor.execute('SHOW transaction_read_only');assert cursor.fetchone()[0]=='on' + cursor.execute('SHOW transaction_isolation');assert cursor.fetchone()[0]=='repeatable read' + checks.append(True) + return original(*args) +import_inspection.observe_unit=observe +import pytest +code=pytest.main(['-q','-p','no:cacheprovider','/app/tests/test_import_receipts.py', + '/app/tests/test_import_inspection.py','/app/tests/test_import_corpus.py', + '/app/tests/test_import_reconciliation.py']) +assert code==0 and checks +print(json.dumps({'status':'passed','database_vendor':'postgresql','server_version':version, + 'inspection_transaction_mode_checks':len(checks),'source_overlay':True, + 'locked_development_dependencies':True,'production_data_used':False})) +""" + + +def docker(*args): + return subprocess.run( + ["docker", *args], capture_output=True, text=True, check=True, timeout=180 + ).stdout.strip() + + +def main(): + app = docker("image", "inspect", IMAGE, "--format", "{{.Id}}") + pg = docker("image", "inspect", "postgres:16", "--format", "{{.Id}}") + network = database = runner = None + name = "csoc-inspection-" + uuid.uuid4().hex[:12] + try: + network = docker("network", "create", "--internal", name) + database = docker( + "create", + "--name", + name + "-db", + "--network", + network, + "--network-alias", + "inspection-db", + "-e", + "POSTGRES_HOST_AUTH_METHOD=trust", + "-e", + "POSTGRES_DB=inspection", + pg, + ) + docker("start", database) + runner = docker( + "create", + "--name", + name + "-app", + "--network", + network, + "--read-only", + "--tmpfs", + "/tmp", + "--entrypoint", + "/app/.venv/bin/python", + "-e", + "PYTHONDONTWRITEBYTECODE=1", + "-e", + "PYTHONPATH=/test-deps", + "-v", + str(ROOT / "coulomb_social") + ":/app/coulomb_social:ro", + "-v", + str(ROOT / "tests") + ":/app/tests:ro", + "-v", + sysconfig.get_path("purelib") + ":/test-deps:ro", + app, + "-c", + CODE, + ) + for identity in (database, runner): + metadata = json.loads(docker("inspect", identity))[0] + if metadata["HostConfig"].get("PortBindings"): + raise RuntimeError("published_ports_refused") + if not json.loads(docker("network", "inspect", network))[0]["Internal"]: + raise RuntimeError("internal_network_required") + result = docker("start", "--attach", runner) + if docker("inspect", runner, "--format", "{{.State.ExitCode}}") != "0": + raise RuntimeError("rehearsal_failed") + proof = json.loads(result.splitlines()[-1]) + finally: + # Delete only IDs returned by this invocation, including their anonymous + # test volumes. Retained recovery containers and networks are unrelated. + for identity in (runner, database): + if identity: + docker("rm", "--force", "--volumes", identity) + if network: + docker("network", "rm", network) + files = [ + Path(__file__), + ROOT / "uv.lock", + ROOT / "coulomb_social/apps/spaces/import_tree.py", + ROOT / "coulomb_social/apps/spaces/import_receipts.py", + ROOT / "coulomb_social/apps/spaces/import_inspection.py", + ROOT / "coulomb_social/apps/spaces/import_reconciliation.py", + ROOT / "coulomb_social/apps/spaces/management/commands/inspect_content_corpus.py", + ROOT / "coulomb_social/apps/spaces/management/commands/reconcile_content_import.py", + *( + ROOT / "tests" / name + for name in ( + "test_import_corpus.py", + "test_import_receipts.py", + "test_import_inspection.py", + "test_import_reconciliation.py", + ) + ), + ] + print( + json.dumps( + { + "schema": "coulomb.corpus-inspection-rehearsal.v1", + "proof": proof, + "app_dependency_image": app, + "postgres_image": pg, + "internal_network": True, + "published_ports": False, + "disposable_resources_removed": True, + "source_sha256": { + str(p.relative_to(ROOT)): hashlib.sha256(p.read_bytes()).hexdigest() + for p in files + }, + }, + indent=2, + ) + ) + + +if __name__ == "__main__": + main() diff --git a/scripts/rehearse-reset-authority.py b/scripts/rehearse-reset-authority.py new file mode 100644 index 0000000..6d86421 --- /dev/null +++ b/scripts/rehearse-reset-authority.py @@ -0,0 +1,156 @@ +#!/usr/bin/env python3 +"""Measure reset rights on disposable native peers; never contact production.""" + +import argparse +import hashlib +import json +import re +import runpy +import secrets +import urllib.parse +import uuid +from datetime import UTC, datetime +from pathlib import Path + +HERE = Path(__file__).resolve().parent +BASE = runpy.run_path(str(HERE / 'rehearse-native-password-reset.py')) +docker, container, archive, probe, configuration = ( + BASE[n] for n in ('docker', 'container', 'archive', 'probe', 'configuration')) + + +def set_password(anchor, token, target, password): + assert target in {'alice', 'bob', 'operator', 'appoperator', 'svc-other', 'svc-auth'} + return docker('exec', '-i', anchor, '/bin/sh', '-c', + 'read -r token; read -r LLDAP_USER_PASSWORD; export LLDAP_USER_PASSWORD; ' + 'exec /app/lldap_set_password --base-url http://127.0.0.1:17170 ' + '--token "$token" --username ' + target, + data=(token + '\n' + password + '\n').encode(), check=False).returncode + + +def rehearse(): + if not __debug__: + raise RuntimeError('acceptance_assertions_must_be_enabled') + context = json.loads(docker('context', 'inspect', 'default').stdout)[0] + assert context['Endpoints']['docker']['Host'] == 'unix:///var/run/docker.sock' + for image in (BASE['LLDAP'], BASE['AUTHELIA'], BASE['CLIENT']): + assert json.loads(docker('image', 'inspect', image).stdout)[0]['Id'] == image + name = 'csoc-reset-authority-' + uuid.uuid4().hex[:12] + auth = name + '-auth' + values = {key: secrets.token_urlsafe(24) for key in ( + 'admin_password', 'bind_password', 'old_password', 'new_password')} + targets = ['alice', 'bob', 'operator', 'appoperator', 'svc-other'] + result = {'scenarios': []} + try: + container(name, BASE['LLDAP'], 'none', { + 'LLDAP_JWT_SECRET_FILE': '/run/fixture/jwt', + 'LLDAP_LDAP_USER_PASS_FILE': '/run/fixture/admin-password', + 'LLDAP_LDAP_BASE_DN': 'dc=example,dc=test', + 'LLDAP_LDAP_HOST': '127.0.0.1', 'LLDAP_HTTP_HOST': '127.0.0.1'}) + docker('exec', '-i', name, 'tar', '-xf', '-', '-C', '/run/fixture', data=archive({ + 'jwt': secrets.token_hex(32).encode(), + 'admin-password': values['admin_password'].encode(), + 'lldap.toml': ('database_url = "sqlite:///data/users.db?mode=rwc"\nkey_seed = ' + + json.dumps(secrets.token_hex(32)) + '\n').encode()})) + docker('exec', '-d', name, '/bin/sh', '-c', + 'exec /app/lldap run --config-file /run/fixture/lldap.toml ' + '> /run/fixture/server.log 2>&1') + prepared = probe(name, {**values, 'mode': 'bootstrap', 'authority_matrix': True}) + assert set_password(name, prepared['admin_token'], 'svc-auth', + values['bind_password']) == 0 + for role in ['lldap_strict_readonly', 'lldap_password_manager']: + probe(name, {**values, 'mode': 'bind_role', 'role': role}) + context = probe(name, {**values, 'mode': 'authority_context'}) + writes = probe(name, {**values, 'mode': 'write_boundaries'}) + scenario = {'bind_role': role, 'directory_writes': writes, + 'direct_password_changes': [], 'authelia_resets': []} + for target in targets: + assert set_password(name, context['admin_token'], target, + values['old_password']) == 0 + before = probe(name, {**values, 'mode': 'directory_authentication', + 'target': target}) + assert before == {'old_password_accepted': True, 'new_password_accepted': False} + code = set_password(name, context['service_token'], target, values['new_password']) + after = probe(name, {**values, 'mode': 'directory_authentication', + 'target': target}) + expected = role == 'lldap_password_manager' and target != 'operator' + assert (code == 0) is expected, 'unexpected_direct_password_right' + assert after == {'old_password_accepted': not expected, + 'new_password_accepted': expected} + scenario['direct_password_changes'].append({ + 'target': target, 'password_changed': expected, **after}) + # Separate fresh stores make reset/link checks independent of earlier scenarios. + for target in ['alice', 'bob', 'operator', 'appoperator']: + assert set_password(name, context['admin_token'], target, + values['old_password']) == 0 + container(auth, BASE['AUTHELIA'], 'container:' + name, {}) + docker('exec', '-i', auth, 'tar', '-xf', '-', '-C', '/run/fixture', data=archive({ + 'configuration.yml': json.dumps( + configuration(values['bind_password'])).encode()})) + docker('exec', '-d', auth, '/bin/sh', '-c', + 'exec /app/authelia --config /run/fixture/configuration.yml ' + '> /run/fixture/server.log 2>&1') + requested = probe(name, {**values, 'mode': 'reset_request', 'target': target}) + notification = docker( + 'exec', auth, 'cat', '/data/notifications.txt').stdout.decode() + links = re.findall(r'https://auth\.recovery\.test/reset-password/step2\?[^\s<>]+', + notification) + assert len(links) == 1, 'one_native_challenge_required' + token = urllib.parse.parse_qs(urllib.parse.urlsplit(links[0]).query)['token'][0] + settled = probe(name, {**values, 'mode': 'reset_finish', 'target': target, + 'reset_token': token}) + expected = role == 'lldap_password_manager' and target != 'operator' + assert settled['new_password_accepted_after'] is expected + assert settled['old_password_accepted_after'] is not expected + assert (settled['status'] == 'reset_completed') is expected + scenario['authelia_resets'].append({'target': target, **requested, **settled}) + docker('rm', '-f', '-v', auth) + result['scenarios'].append(scenario) + finally: + for current in [name + '-probe', auth, name]: + docker('rm', '-f', '-v', current, check=False) + result['cleanup_verified'] = all(not docker( + 'ps', '-aq', '--filter', 'name=^/' + n + '$').stdout.strip() + for n in [name + '-probe', auth, name]) + if not result['cleanup_verified']: + raise RuntimeError('fixture_cleanup_requires_review') + assert not any(value in json.dumps(result) for value in values.values()) + return result + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--output', type=Path, required=True) + args = parser.parse_args() + manifest_path = args.output.with_suffix('.sources.txt') + if args.output.exists() or manifest_path.exists(): + raise SystemExit('Fresh receipt and source manifest required') + findings = rehearse() + paths = [Path(__file__), HERE / 'rehearse-native-password-reset.py', + HERE / 'native_recovery/probe.py'] + manifest = ''.join(str(p.relative_to(HERE.parent)) + ': ' + + hashlib.sha256(p.read_bytes()).hexdigest() + '\n' for p in paths).encode() + receipt = {'schema': 'coulomb.native-reset-authority-matrix.v1', + 'observed_at': datetime.now(UTC).isoformat(), 'data': 'synthetic', + 'status': 'native_authority_matrix_verified', + 'images': {n: BASE[n.upper()] for n in ('lldap', 'authelia', 'client')}, + 'production_access': False, 'provider_smtp_contacted': False, + 'owner_runtime_or_permission_change_performed': False, + 'no_ports_host_mounts_or_external_routes': True, + 'backend_container_flags_verified_before_fixture_import': True, + 'source_manifest_sha256': hashlib.sha256(manifest).hexdigest(), + 'target_classes': {'alice': 'ordinary user', 'bob': 'other ordinary user', + 'operator': 'lldap_admin member', + 'appoperator': 'application-admins member, not lldap_admin', + 'svc-other': 'other service, lldap_strict_readonly'}, + 'findings': findings} + with manifest_path.open('xb') as stream: + stream.write(manifest) + with args.output.open('x') as stream: + json.dump(receipt, stream, indent=2) + stream.write('\n') + print(json.dumps({'status': receipt['status'], 'roles': len(findings['scenarios']), + 'cleanup_verified': findings['cleanup_verified']})) + + +if __name__ == '__main__': + main() diff --git a/scripts/rehearse-reset-role-refresh.py b/scripts/rehearse-reset-role-refresh.py new file mode 100644 index 0000000..8a38167 --- /dev/null +++ b/scripts/rehearse-reset-role-refresh.py @@ -0,0 +1,147 @@ +#!/usr/bin/env python3 +"""Verify role changes with one running Authelia process; synthetic peers only.""" + +import argparse +import hashlib +import json +import re +import runpy +import secrets +import urllib.parse +import uuid +from datetime import UTC, datetime +from pathlib import Path + +HERE = Path(__file__).resolve().parent +BASE = runpy.run_path(str(HERE / 'rehearse-native-password-reset.py')) +docker, container, archive, probe, configuration = ( + BASE[n] for n in ('docker', 'container', 'archive', 'probe', 'configuration')) + + +def set_password(anchor, token, target, password): + assert target in {'alice', 'bob', 'operator', 'appoperator', 'svc-other', 'svc-auth'} + return docker('exec', '-i', anchor, '/bin/sh', '-c', + 'read -r token; read -r LLDAP_USER_PASSWORD; export LLDAP_USER_PASSWORD; ' + 'exec /app/lldap_set_password --base-url http://127.0.0.1:17170 ' + '--token "$token" --username ' + target, + data=(token + '\n' + password + '\n').encode(), check=False).returncode + + +def rehearse(): + if not __debug__: + raise RuntimeError('acceptance_assertions_must_be_enabled') + context = json.loads(docker('context', 'inspect', 'default').stdout)[0] + assert context['Endpoints']['docker']['Host'] == 'unix:///var/run/docker.sock' + for image in (BASE['LLDAP'], BASE['AUTHELIA'], BASE['CLIENT']): + assert json.loads(docker('image', 'inspect', image).stdout)[0]['Id'] == image + name = 'csoc-reset-transition-' + uuid.uuid4().hex[:12] + auth = name + '-auth' + values = {key: secrets.token_urlsafe(24) for key in ( + 'admin_password', 'bind_password', 'old_password', 'new_password')} + result = {'scenarios': []} + try: + container(name, BASE['LLDAP'], 'none', { + 'LLDAP_JWT_SECRET_FILE': '/run/fixture/jwt', + 'LLDAP_LDAP_USER_PASS_FILE': '/run/fixture/admin-password', + 'LLDAP_LDAP_BASE_DN': 'dc=example,dc=test', + 'LLDAP_LDAP_HOST': '127.0.0.1', 'LLDAP_HTTP_HOST': '127.0.0.1'}) + docker('exec', '-i', name, 'tar', '-xf', '-', '-C', '/run/fixture', data=archive({ + 'jwt': secrets.token_hex(32).encode(), + 'admin-password': values['admin_password'].encode(), + 'lldap.toml': ('database_url = "sqlite:///data/users.db?mode=rwc"\nkey_seed = ' + + json.dumps(secrets.token_hex(32)) + '\n').encode()})) + docker('exec', '-d', name, '/bin/sh', '-c', + 'exec /app/lldap run --config-file /run/fixture/lldap.toml ' + '> /run/fixture/server.log 2>&1') + prepared = probe(name, {**values, 'mode': 'bootstrap', 'authority_matrix': True}) + assert set_password(name, prepared['admin_token'], 'svc-auth', + values['bind_password']) == 0 + container(auth, BASE['AUTHELIA'], 'container:' + name, {}) + docker('exec', '-i', auth, 'tar', '-xf', '-', '-C', '/run/fixture', data=archive({ + 'configuration.yml': json.dumps(configuration(values['bind_password'])).encode()})) + # One Authelia process for all three phases, including rollback. + probe(name, {**values, 'mode': 'bind_role', 'role': 'lldap_strict_readonly'}) + docker('exec', '-d', auth, '/bin/sh', '-c', + 'exec /app/authelia --config /run/fixture/configuration.yml ' + '> /run/fixture/server.log 2>&1') + process_marker = None + tokens = set() + for index, role in enumerate(['lldap_strict_readonly', 'lldap_password_manager', + 'lldap_strict_readonly']): + if index: + probe(name, {**values, 'mode': 'bind_role', 'role': role}) + assert set_password(name, prepared['admin_token'], 'alice', + values['old_password']) == 0 + docker('exec', auth, 'rm', '-f', '/data/notifications.txt') + requested = probe(name, {**values, 'mode': 'reset_request', 'target': 'alice'}) + marker = docker('exec', auth, '/bin/sh', '-c', + 'for p in /proc/[0-9]*; do ' + 'if [ "$(cat "$p/comm" 2>/dev/null)" = authelia ]; ' + 'then cat "$p/stat"; fi; done').stdout + # /proc stat changes CPU counters; compare PID and start ticks only. + fields = marker.decode().split() + current_marker = (fields[0], fields[21]) + if process_marker is None: + process_marker = current_marker + assert current_marker == process_marker, 'authelia_process_restarted' + notification = docker('exec', auth, 'cat', '/data/notifications.txt').stdout.decode() + links = re.findall(r'https://auth\.recovery\.test/reset-password/step2\?[^\s<>]+', + notification) + assert len(links) == 1, ('one_new_native_challenge_required_phase_' + + str(index) + '_count_' + str(len(links))) + token = urllib.parse.parse_qs(urllib.parse.urlsplit(links[-1]).query)['token'][0] + assert token not in tokens, 'fresh_challenge_required' + tokens.add(token) + settled = probe(name, {**values, 'mode': 'reset_finish', 'target': 'alice', + 'reset_token': token}) + expected = role == 'lldap_password_manager' + assert settled['new_password_accepted_after'] is expected + assert settled['old_password_accepted_after'] is not expected + assert (settled['status'] == 'reset_completed') is expected + result['scenarios'].append({'bind_role': role, 'same_authelia_process': True, + **requested, **settled}) + finally: + for current in [name + '-probe', auth, name]: + docker('rm', '-f', '-v', current, check=False) + result['cleanup_verified'] = all(not docker( + 'ps', '-aq', '--filter', 'name=^/' + n + '$').stdout.strip() + for n in [name + '-probe', auth, name]) + if not result['cleanup_verified']: + raise RuntimeError('fixture_cleanup_requires_review') + assert not any(value in json.dumps(result) for value in values.values()) + return result + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--output', type=Path, required=True) + args = parser.parse_args() + manifest_path = args.output.with_suffix('.sources.txt') + if args.output.exists() or manifest_path.exists(): + raise SystemExit('Fresh receipt and source manifest required') + findings = rehearse() + paths = [Path(__file__), HERE / 'rehearse-native-password-reset.py', + HERE / 'native_recovery/probe.py'] + manifest = ''.join(str(p.relative_to(HERE.parent)) + ': ' + + hashlib.sha256(p.read_bytes()).hexdigest() + '\n' for p in paths).encode() + receipt = {'schema': 'coulomb.native-reset-role-refresh.v1', + 'observed_at': datetime.now(UTC).isoformat(), 'data': 'synthetic', + 'status': 'native_role_refresh_verified', + 'images': {n: BASE[n.upper()] for n in ('lldap', 'authelia', 'client')}, + 'production_access': False, 'provider_smtp_contacted': False, + 'owner_runtime_or_permission_change_performed': False, + 'no_ports_host_mounts_or_external_routes': True, + 'backend_container_flags_verified_before_fixture_import': True, + 'source_manifest_sha256': hashlib.sha256(manifest).hexdigest(), + 'findings': findings} + with manifest_path.open('xb') as stream: + stream.write(manifest) + with args.output.open('x') as stream: + json.dump(receipt, stream, indent=2) + stream.write('\n') + print(json.dumps({'status': receipt['status'], 'roles': len(findings['scenarios']), + 'cleanup_verified': findings['cleanup_verified']})) + + +if __name__ == '__main__': + main() diff --git a/scripts/snapshot-content-and-db.py b/scripts/snapshot-content-and-db.py index a928c1b..342510b 100644 --- a/scripts/snapshot-content-and-db.py +++ b/scripts/snapshot-content-and-db.py @@ -21,7 +21,7 @@ from pathlib import Path NS = 'coulomb-social' IMAGE = ('forgejo.coulomb.social/coulomb/coulomb-social@sha256:' - '522bcd7fce92436765a60c1e4afa2598f3cc40913352153ff1c4319a99ca92ed') + 'b58d858abd5b4acbd6b893c3f622f7a9fdb5e606164ab9866eb3cfaa04df1a0e') CLUSTER_UID = 'a553c742-0115-43d4-99a4-a5ca56fe0786' LOCK_PATH = Path('/var/lib/coulomb-social-backup/snapshot.lock') diff --git a/scripts/test-attachments-browser.mjs b/scripts/test-attachments-browser.mjs new file mode 100644 index 0000000..7e26d55 --- /dev/null +++ b/scripts/test-attachments-browser.mjs @@ -0,0 +1,123 @@ +import assert from 'node:assert/strict'; +import {chromium} from 'playwright'; + +const base = new URL(process.argv[2]); +assert.equal(base.protocol, 'http:'); +assert.ok(['localhost', '127.0.0.1'].includes(base.hostname)); +const browser = await chromium.launch({headless: true}); +try { + const context = await browser.newContext(); + const unexpected = []; + await context.route('**/*', route => { + if (new URL(route.request().url()).origin === base.origin) return route.continue(); + unexpected.push(route.request().url()); + return route.abort(); + }); + const page = await context.newPage(); + page.setDefaultTimeout(10000); + async function login(subject) { + await page.goto(`${base.origin}/auth/dev-login/`); + await page.getByLabel('Subject').fill(subject); + await page.getByLabel('Tenant').fill('tenant:attachment-test'); + await page.getByRole('button', {name: 'Establish session'}).click(); + await page.waitForURL('**/app/'); + } + const shared = `${base.origin}/app/spaces/attachments-shared/`; + const destination = `${base.origin}/app/spaces/attachments-private/`; + const source = `${shared}?page=notes`; + async function menu(url, author) { + await page.goto(url); + await page.locator('summary', {hasText: 'Attachments'}).click(); + await page.getByRole('link', {name: author + ? 'Open, download or manage attachments' : 'Open or download attachments', exact: true}).click(); + await page.waitForURL('**/attachments/'); + } + async function downloadedBytes(tab = page) { + const pending = tab.waitForEvent('download'); + await tab.getByRole('link', {name: 'Download', exact: true}).click(); + const stream = await (await pending).createReadStream(); + const chunks = []; + for await (const chunk of stream) chunks.push(chunk); + return Buffer.concat(chunks).toString(); + } + await login('file-author'); + await menu(source, true); + await page.getByLabel('File (up to 16 MiB)').setInputFiles({ + name: 'meeting-notes.txt', mimeType: 'text/plain', buffer: Buffer.from('Synthetic attachment bytes'), + }); + await page.getByRole('button', {name: 'Add attachment', exact: true}).click(); + await page.waitForURL('**/attachments/'); + assert.match(await page.locator('main').innerText(), /Attachment added/); + const originalDownload = await page.getByRole('link', {name: 'Download', exact: true}).getAttribute('href'); + assert.equal(await downloadedBytes(), 'Synthetic attachment bytes'); + const editUrl = `${shared}pages/notes/edit/`; + await page.goto(editUrl); + const files = page.getByRole('region', {name: 'Attachments', exact: true}); + assert.match(await files.innerText(), /meeting-notes.txt/); + assert.equal(await downloadedBytes(), 'Synthetic attachment bytes'); + await page.getByLabel('Body (markdown)').fill('Unsaved author notes'); + const opened = context.waitForEvent('page'); + await page.getByRole('link', {name: 'Manage attachments (new tab)', exact: true}).click(); + const management = await opened; + await management.waitForURL('**/attachments/'); + assert.equal(await page.getByLabel('Body (markdown)').inputValue(), 'Unsaved author notes'); + assert.match(await management.locator('main').innerText(), /meeting-notes.txt/); + await management.close(); + const stale = await context.newPage(); + await stale.goto(editUrl); + assert.match(await stale.getByRole('region', {name: 'Attachments', exact: true}).innerText(), /meeting-notes.txt/); + await stale.getByLabel('Body (markdown)').fill('Keep my stale edit'); + await page.getByRole('button', {name: 'Save page', exact: true}).click(); + await page.waitForURL('**?page=notes'); + await stale.getByRole('button', {name: 'Save page', exact: true}).click(); + await stale.waitForURL('**/edit/'); + assert.match(await stale.locator('main').innerText(), /This page changed/); + assert.equal(await stale.getByLabel('Body (markdown)').inputValue(), 'Keep my stale edit'); + assert.match(await stale.getByRole('region', {name: 'Attachments', exact: true}).innerText(), /meeting-notes.txt/); + assert.equal(await downloadedBytes(stale), 'Synthetic attachment bytes'); + await stale.close(); + await page.goto(editUrl); + assert.equal(await page.getByLabel('Body (markdown)').inputValue(), 'Unsaved author notes\n'); + assert.match(await files.innerText(), /meeting-notes.txt/); + await login('file-reader'); + await menu(source, false); + assert.equal(await page.getByRole('button', {name: 'Add attachment', exact: true}).count(), 0); + assert.equal(await page.getByRole('button', {name: 'Remove from page', exact: true}).count(), 0); + assert.equal(await downloadedBytes(), 'Synthetic attachment bytes'); + await login('file-outsider'); + assert.equal((await page.goto(new URL(originalDownload, base).href)).status(), 404); + assert.equal((await page.goto(`${shared}pages/notes/attachments/`)).status(), 404); + await login('file-author'); + await page.goto(source); + await page.getByRole('link', {name: 'Duplicate page', exact: true}).click(); + await page.getByLabel('Destination space').selectOption('attachments-private'); + await page.getByLabel('Title', {exact: false}).fill('Independent files'); + await page.getByLabel('New page slug', {exact: false}).fill('copied'); + await page.getByRole('button', {name: 'Create duplicate'}).click(); + await page.waitForURL('**?page=copied'); + await menu(`${destination}?page=copied`, true); + assert.equal(await downloadedBytes(), 'Synthetic attachment bytes'); + const copiedDownload = await page.getByRole('link', {name: 'Download', exact: true}).getAttribute('href'); + assert.notEqual(copiedDownload, originalDownload); + await page.getByRole('button', {name: 'Remove from page', exact: true}).click(); + await page.waitForURL('**/attachments/'); + assert.match(await page.locator('main').innerText(), /No attachments on this page/); + await menu(source, true); + assert.equal(await downloadedBytes(), 'Synthetic attachment bytes'); + await page.goto(source); + await page.getByRole('link', {name: 'Transfer', exact: true}).click(); + await page.getByLabel('Destination space').selectOption('attachments-private'); + await page.getByLabel('Slug in destination', {exact: false}).fill('moved'); + await page.getByRole('button', {name: 'Transfer page', exact: true}).click(); + await page.waitForURL('**?page=moved'); + await menu(`${destination}?page=moved`, true); + assert.equal(await downloadedBytes(), 'Synthetic attachment bytes'); + const movedDownload = await page.getByRole('link', {name: 'Download', exact: true}).getAttribute('href'); + await login('file-reader'); + assert.equal((await page.goto(source)).status(), 404); + assert.equal((await page.goto(new URL(movedDownload, base).href)).status(), 404); + assert.deepEqual(unexpected, []); + console.log('Attachment menu upload, download, independent copy, move and removal passed'); +} finally { + await browser.close(); +} diff --git a/scripts/tests/bubble-resource-utils.test.mjs b/scripts/tests/bubble-resource-utils.test.mjs new file mode 100644 index 0000000..2d1a8e0 --- /dev/null +++ b/scripts/tests/bubble-resource-utils.test.mjs @@ -0,0 +1,87 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import {ingestResourceBody, resourceParents, attachmentRows} from '../bubble-resource-utils.mjs'; +const id='1700000000000x1', other='1700000000000x2'; +const output=()=>({records:{},empty:{}}); + +test('only explicit affected parents are visited; descriptors are not article targets',()=>{ + assert.deepEqual(resourceParents({[id]:{_list_custom_scrap:'LOOKUP__'+other}, + [other]:{hasattachments_boolean:true}, '1700000000000x3':{_list_custom_scrap:[]}, + invalid:{hasattachments_boolean:true}}),[id,other]); +}); + +test('typed search and initialization resource observations preserve versions',()=>{ + const result=output(); + ingestResourceBody(JSON.stringify({responses:[{hits:{hits:[{_type:'custom.scrap',_id:id, + _source:{content_text:'Synthetic first'}}]}}]}),result); + const envelope={type:'custom.scrap',id,data:{_type:'custom.scrap',_id:id, + content_text:'Synthetic second'}}; + ingestResourceBody(JSON.stringify(envelope),result); + ingestResourceBody(JSON.stringify(envelope),result); + assert.equal(result.records['custom.scrap:'+id].length,2); + assert.equal(result.records['custom.scrap:'+id][0].content_text,'Synthetic first'); +}); + +test('empty envelopes remain unresolved; mixed ids/types and nested users are excluded',()=>{ + const result=output(); + const resource={_type:'custom.scrap',_id:id,_source:{content_text:'Synthetic'}}; + ingestResourceBody(JSON.stringify([ + {type:'custom.scrap',id,data:{}}, + {type:'custom.scrap',id,data:{_type:'custom.scrap',_id:other}}, + {type:'user',id,data:resource}, + {_type:'user',_id:id,_source:resource}, + {...resource,_source:{_type:'custom.article'}}, + {...resource,_source:{_id:other}}, + {_type:'custom.article',_id:id,_source:{content_text:'Synthetic article'}}, + ]),result); + assert.deepEqual(result.records,{}); + assert.deepEqual(result.empty,{'custom.scrap:1700000000000x1':{type:'custom.scrap',id}}); +}); + +test('NDJSON selects only content-resource types',()=>{ + const result=output(); + ingestResourceBody('unrelated\n'+JSON.stringify({_type:'custom.relattachment',_id:id, + _source:{url_text:'https://example.invalid/synthetic'}})+'\n'+ + JSON.stringify({_type:'session',_id:other,_source:{cookie:'synthetic'}}),result); + assert.deepEqual(Object.keys(result.records),['custom.relattachment:'+id]); +}); + +function attachmentFixture() { + const relation={_type:'custom.relattachment',_id:'1700000000000x3', + to_custom_article:id,_chunk_custom_article:other}; + const resources={records:{'custom.relattachment:1700000000000x3':[relation]}}; + const articles={[other]:{context_custom_article:id,nomer_text:'Synthetic file', + logo_image:'//0123456789abcdef0123456789abcdef.cdn.bubble.io/synthetic.pdf', + 'Created Date':1700000000000,'Modified Date':1700000000001}}; + return {resources,articles}; +} + +test('attachment relation binds parent, original file and exact source dates',()=>{ + const {resources,articles}=attachmentFixture(); + const [row]=attachmentRows(id,resources,articles); + assert.equal(row.article_id,other); + assert.equal(row.url,'https:'+articles[other].logo_image); + assert.equal(row.created_at,1700000000000); + assert.equal(row.modified_at,1700000000001); + assert.deepEqual(attachmentRows('1700000000000x9',resources,articles),[]); +}); + +test('incomplete, cross-parent and drifting attachment relations are refused',()=>{ + for(const change of [f=>delete f.articles[other], + f=>{f.articles[other].context_custom_article='1700000000000x9';}, + f=>{f.resources.records['custom.relattachment:1700000000000x3'].push( + {...f.resources.records['custom.relattachment:1700000000000x3'][0],_version:2});}]) { + const f=attachmentFixture();change(f); + assert.throws(()=>attachmentRows(id,f.resources,f.articles),/attachment_/); + } +}); + +test('file retrieval requires the observed HTTPS Bubble CDN, without credentials',()=>{ + for(const url of ['http://0123456789abcdef0123456789abcdef.cdn.bubble.io/file', + 'https://example.invalid/file','https://localhost/file', + 'https://secret@0123456789abcdef0123456789abcdef.cdn.bubble.io/file', + 'https://0123456789abcdef0123456789abcdef.cdn.bubble.io:444/file']) { + const f=attachmentFixture();f.articles[other].logo_image=url; + assert.throws(()=>attachmentRows(id,f.resources,f.articles),/attachment_file_origin/); + } +}); diff --git a/scripts/verify-bubble-body-resources.py b/scripts/verify-bubble-body-resources.py new file mode 100644 index 0000000..07bc0f4 --- /dev/null +++ b/scripts/verify-bubble-body-resources.py @@ -0,0 +1,144 @@ +#!/usr/bin/env python3 +"""Verify retained body references through the product renderer, without network or import.""" + +import argparse +import hashlib +import importlib.util +import json +import os +import sys +from collections import Counter +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(ROOT)) +os.environ['DJANGO_SETTINGS_MODULE'] = 'coulomb_social.settings.test' +import django # noqa: E402 + +django.setup() +from coulomb_social.apps.spaces.bubble_bundle import prepare_bundle # noqa: E402 +from coulomb_social.apps.spaces.rendering_audit import audit_body # noqa: E402 + +# Same bounded, owned-file and no-symlink reader used for the independent +# Visual-only preservation proof. It performs no initialization or network I/O. +SPEC = importlib.util.spec_from_file_location( + 'visual_preservation', ROOT / 'scripts/verify-bubble-visual-preservation.py') +reader = importlib.util.module_from_spec(SPEC) +SPEC.loader.exec_module(reader) + + +def sha(raw): + return hashlib.sha256(raw).hexdigest() + + +def verify(original, prepared): + raw_report = reader.read(original, 'reconciliation.json') + before = json.loads(raw_report) + visual_raw = reader.read(prepared, 'reconciliation.json') + visual = json.loads(visual_raw) + if (before['schema'] != 'coulomb.bubble-review-bundle.v1' + or before['status'] != 'completed_offline_preparation' + or visual['original_report_sha256'] != sha(raw_report)): + raise ValueError('source report mismatch') + inputs = {} + for name in ['spaces', 'articles', 'retained_spaces']: + raw = reader.read(original, 'source-capture/' + name + '.json') + if sha(raw) != before['source_sha256'][name]: + raise ValueError('source digest mismatch') + inputs[name] = json.loads(raw) + fresh, files = prepare_bundle(inputs['spaces'], inputs['articles'], + inventory_spaces=inputs['retained_spaces']) + # New report guards must not alter the original draft content or sidecars. + if {name: sha(raw) for name, raw in files.items()} != before['file_sha256']: + raise ValueError('draft file transformation changed') + for name, raw in files.items(): + if reader.read(original, name) != raw: + raise ValueError('original draft file drift') + bodies = {} + for article in fresh['articles']: + if article['body_file']: + raw = reader.read(original, article['body_file']) + if sha(raw) != article['body_sha256']: + raise ValueError('body sidecar drift') + bodies[article['id']] = raw + prepared_pages = 0 + for space in visual['spaces']: + for page in space['articles']: + if page['file']: + raw = reader.read(prepared, page['file']) + if sha(raw) != visual['file_sha256'][page['file']]: + raise ValueError('prepared page drift') + if raw.split(b'\n---\n', 1)[1] != bodies[page['id']]: + raise ValueError('prepared source body changed') + prepared_pages += 1 + details = [] + for article in fresh['articles']: + detail = {'id': article['id'], 'body_present': article['id'] in bodies, + 'references': [r for r in article['references'] if r['kind'] != 'visual'], + 'review_flags': article['review_flags']} + if 'scrap_relation' in article: + detail['scrap_relation'] = article['scrap_relation'] + if detail['body_present']: + detail['rendering'], _ = audit_body(bodies[article['id']].decode()) + details.append(detail) + rendered = [a['rendering'] for a in details if a['body_present']] + refs = [r for a in details for r in a['references']] + summary = { + 'schema': 'coulomb.bubble-body-resource-proof.v1', + 'source_report_sha256': sha(raw_report), 'visual_report_sha256': sha(visual_raw), + 'source_sha256': before['source_sha256'], + 'captured_articles': len(details), 'source_bodies_verified': len(bodies), + 'missing_bodies': len(details) - len(bodies), 'prepared_pages_verified': prepared_pages, + 'original_draft_files_recompiled_unchanged': len(files), + 'body_reference_inventory_entries': len(refs), + 'body_reference_records': sum(bool(a['references']) for a in details), + 'body_reference_classifications': dict(Counter(r['classification'] for r in refs)), + 'reference_occurrences_before': sum(r['reference_occurrences_before'] for r in rendered), + 'reference_occurrences_after': sum(r['reference_occurrences_after'] for r in rendered), + 'records_with_changed_reference_attributes': sum( + not r['reference_attributes_preserved'] for r in rendered), + 'unresolved_scrap_relation_records': sum( + a.get('scrap_relation', {}).get('state') == 'unresolved' for a in details), + 'attachment_flag_records': sum('attachment_records_require_capture' in a['review_flags'] + for a in details), + 'live_destinations_checked': False, 'original_bubble_render_compared': False, + 'productive_fidelity_accepted': False, 'production_imported': False, + 'network_requests': 0, + } + return summary, {'summary': summary, 'records': details, + 'fresh_space_blockers': [{'id': s['id'], 'blockers': s['blockers']} + for s in fresh['spaces']]} + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('original', type=Path) + parser.add_argument('prepared', type=Path) + parser.add_argument('--output', type=Path, required=True, help='New private JSON outside repo') + args = parser.parse_args() + try: + out = args.output + if (out.exists() or any(p.is_symlink() for p in (out, *out.parents)) + or any(out.resolve().is_relative_to(p.resolve()) + for p in [ROOT, args.original, args.prepared])): + raise ValueError('separate new output required') + info = out.parent.stat() + if info.st_uid != os.getuid() or info.st_mode & 0o077: + raise ValueError('private owned output parent required') + summary, private = verify(args.original, args.prepared) + raw = json.dumps(private, indent=2).encode() + with os.fdopen(os.open(out, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600), 'wb') as f: + f.write(raw) + f.flush() + os.fsync(f.fileno()) + summary['private_report_sha256'] = sha(raw) + print(json.dumps(summary)) + except Exception: + print('Body resource proof refused; no completeness or acceptance inferred.', + file=sys.stderr) + return 1 + return 0 + + +if __name__ == '__main__': + raise SystemExit(main()) diff --git a/scripts/verify-bubble-visual-preservation.py b/scripts/verify-bubble-visual-preservation.py new file mode 100644 index 0000000..ca4587c --- /dev/null +++ b/scripts/verify-bubble-visual-preservation.py @@ -0,0 +1,115 @@ +#!/usr/bin/env python3 +"""Independently verify Visual-only frontmatter changes against the original private bundle.""" + +import argparse +import hashlib +import json +import os +import re +from pathlib import Path + + +def read(root, name): + path = root / name + if ( + Path(name).is_absolute() + or ".." in Path(name).parts + or any(p.is_symlink() for p in (path, *path.parents)) + ): + raise ValueError("unsafe source path") + info = path.stat() + if ( + not path.is_file() + or info.st_uid != os.getuid() + or info.st_mode & 0o077 + or info.st_nlink != 1 + or info.st_size > 16 * 1024 * 1024 + ): + raise ValueError("private bounded source required") + return path.read_bytes() + + +def verify(source, out): + if not __debug__: + raise ValueError("verification assertions required") + old = json.loads(read(source, "reconciliation.json")) + new = json.loads(read(out, "reconciliation.json")) + def sha(raw): + return hashlib.sha256(raw).hexdigest() + assert new["original_report_sha256"] == sha(read(source, "reconciliation.json")) + assert read(out, "source-capture/reconciliation-before-visuals.json") == read( + source, "reconciliation.json" + ) + for name, digest in new["file_sha256"].items(): + assert sha(read(out, name)) == digest + changed = 0 + for name, digest in old["file_sha256"].items(): + original = read(source, name) + current = read(out, name) + assert sha(original) == digest + if current != original: + assert "/pages/" in name and name.endswith(".md") + before, body = original.split(b"\n---\n", 1) + after, after_body = current.split(b"\n---\n", 1) + assert body == after_body + assert ( + len(re.findall(rb'^visual: "assets/[a-f0-9]{64}\.(?:png|jpg|webp)"$', after, re.M)) + == 1 + ) + if name.endswith("/index.md"): + restored = re.sub( + rb'\nvisual: "assets/[a-f0-9]{64}\.(?:png|jpg|webp)"$', b"", after + ) + else: + restored = re.sub( + rb'^visual: "assets/[a-f0-9]{64}\.(?:png|jpg|webp)"$', + b'visual: ""', + after, + flags=re.M, + ) + assert restored == before + changed += 1 + for axis in ["unassigned_article_ids", "source_sha256"]: + assert new[axis] == old[axis] + assert len(old["spaces"]) == len(new["spaces"]) + for a, b in zip(old["spaces"], new["spaces"], strict=True): + assert a["blockers"] == b["blockers"] and not b["import_ready"] + for key in a: + if key != "articles": + assert a[key] == b[key], key + for x, y in zip(a["articles"], b["articles"], strict=True): + assert {k: v for k, v in x.items() if k != "file_sha256"} == { + k: v for k, v in y.items() if k != "file_sha256" + } + for p in out.rglob("*"): + assert p.stat().st_mode & 0o077 == 0 + return { + "schema": "coulomb.visual-preservation-proof.v1", + "verified_output_files": len(new["file_sha256"]), + "verified_original_files": len(old["file_sha256"]), + "frontmatter_only_changes": changed, + "all_source_bodies_and_dates_preserved": True, + "spaces_preserved": len(new["spaces"]), + "original_blockers_preserved": True, + "unassigned_records_preserved": True, + "private_modes_verified": True, + "private_bundle_sha256": sha(read(out, "reconciliation.json")), + "production_imported": False, + } + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("original", type=Path) + parser.add_argument("prepared", type=Path) + args = parser.parse_args() + try: + print(json.dumps(verify(args.original, args.prepared))) + except Exception: + print("Private visual preservation verification failed; no acceptance inferred.") + return 1 + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/verify-bubble-visuals.mjs b/scripts/verify-bubble-visuals.mjs new file mode 100644 index 0000000..9ea4d64 --- /dev/null +++ b/scripts/verify-bubble-visuals.mjs @@ -0,0 +1,49 @@ +#!/usr/bin/env node +/** Decode only locally retained Visual files in an offline disposable browser; emit counts only. */ +import { readFileSync, lstatSync, realpathSync } from 'node:fs'; +import { resolve, join } from 'node:path'; +import { createHash } from 'node:crypto'; +import { chromium } from 'playwright'; + +let browser; +try { + const root = resolve(process.argv[2]); + const reportBytes = readFileSync(join(root, 'reconciliation.json')); + const report = JSON.parse(reportBytes); + if (report.status !== 'completed_private_visual_preparation') throw new Error('unfinished_bundle'); + const entries = Object.entries(report.file_sha256).filter(([name]) => name.startsWith('captured-visuals/')); + browser = await chromium.launch({ headless: true }); + const context = await browser.newContext({ serviceWorkers: 'block', offline: true }); + await context.route('**/*', route => route.abort()); + const page = await context.newPage(); + let decoded = 0; + for (const [name, hash] of entries) { + if (!/^captured-visuals\/[a-f0-9]{64}\.(png|jpg|webp)$/.test(name)) throw new Error('invalid_asset_path'); + const path = join(root, name), info = lstatSync(path); + if (!info.isFile() || info.isSymbolicLink() || info.size > 16 * 1024 * 1024 || + (info.mode & 0o077) || info.uid !== process.getuid() || + realpathSync(path) !== path) throw new Error('unsafe_asset'); + const data = readFileSync(path); + if (createHash('sha256').update(data).digest('hex') !== hash) throw new Error('changed_asset'); + const mime = name.endsWith('.jpg') ? 'image/jpeg' : name.endsWith('.webp') ? 'image/webp' : 'image/png'; + const ok = await page.evaluate(async ({data, mime}) => { + const bytes = Uint8Array.from(atob(data), x => x.charCodeAt(0)); + const url = URL.createObjectURL(new Blob([bytes], {type: mime})); + try { + const img = new Image(); img.src = url; + await Promise.race([img.decode(), new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), 10000))]); + return img.naturalWidth > 0 && img.naturalHeight > 0; + } finally { URL.revokeObjectURL(url); } + }, {data: data.toString('base64'), mime}); + if (!ok) throw new Error('image_decode_failed'); + decoded++; + } + console.log(JSON.stringify({schema: 'coulomb.private-visual-decode.v1', status: 'all_captured_visuals_decoded', + decoded_images: decoded, bundle_sha256: createHash('sha256').update(reportBytes).digest('hex'), + browser_network_disabled: true, productive_fidelity_accepted: false})); +} catch { + console.error('Private visual decode verification failed; no acceptance inferred.'); + process.exitCode = 1; +} finally { + if (browser) await browser.close(); +} diff --git a/tests/test_attachments.py b/tests/test_attachments.py new file mode 100644 index 0000000..4f9f270 --- /dev/null +++ b/tests/test_attachments.py @@ -0,0 +1,256 @@ +"""Author-managed attachments preserve permissions, revisions and crash recovery.""" + +import os +import subprocess +import sys + +import pytest +from django.core.files.uploadedfile import SimpleUploadedFile +from django.test import Client +from django.urls import reverse + +from coulomb_social.apps.spaces import attachments, pageops +from coulomb_social.apps.spaces.assets import local_download_url +from coulomb_social.apps.spaces.content import load_space_page +from coulomb_social.apps.spaces.models import SpaceMembership +from coulomb_social.apps.spaces.services import create_space, put_space_page +from tests.test_space_permissions import make_member + +pytestmark = pytest.mark.django_db + + +@pytest.fixture +def people(tmp_path, settings, client): + settings.CONTENT_ROOT = str(tmp_path / "content") + owner, reader, outsider = [make_member(name) for name in ("author", "reader", "outsider")] + space = create_space(owner, title="Shared files", slug="shared-files") + SpaceMembership.objects.create(space=space, member=reader, role="viewer") + doc = put_space_page(owner, space, "notes", title="Notes", body="Retained notes", + extra_meta={"source_created_at": "2020-01-02T03:04:05Z"}) + client.force_login(owner.user) + return space, doc, owner, reader, outsider + + +def upload(space, doc, name="Report.pdf", data=b"synthetic PDF"): + return attachments.add(space.slug, doc.slug, SimpleUploadedFile(name, data), + revision=doc.revision) + + +def test_upload_download_and_remove_preserve_source_and_retained_revisions(people, client): + space, doc, _, reader, _ = people + url = reverse("spaces:page_attachments", args=[space.slug, doc.slug]) + result = client.post(url, {"action": "add", "revision": doc.revision, + "file": SimpleUploadedFile("Report.pdf", b"synthetic PDF")}) + assert result.status_code == 302 + added = pageops.read_page(space.slug, doc.slug) + entry, = attachments.entries(added.meta) + assert entry["name"] == "Report.pdf" + assert added.body == doc.body + assert added.meta["source_created_at"] == doc.meta["source_created_at"] + client.force_login(reader.user) + response = client.get(local_download_url(space.slug, entry["path"])) + assert response.status_code == 200 + assert response["Content-Type"] == "application/octet-stream" + assert response["Content-Disposition"].startswith("attachment;") + assert response["Cache-Control"] == "private, no-store" + assert response["X-Content-Type-Options"] == "nosniff" + assert b"".join(response.streaming_content) == b"synthetic PDF" + client.force_login(people[2].user) + assert client.post(url, {"action": "remove", "revision": added.revision, + "reference": entry["path"]}).status_code == 302 + current = pageops.read_page(space.slug, doc.slug) + assert attachments.entries(current.meta) == [] and current.body == doc.body + assert (pageops.space_dir(space.slug) / entry["path"]).read_bytes() == b"synthetic PDF" + retained = pageops.content_root() / ".revisions" / space.slug / doc.slug + assert (retained / (doc.revision + ".md")).is_file() + assert entry["path"] in (retained / (added.revision + ".md")).read_text() + + +def test_attachment_permissions_and_revocation_cover_reads_and_mutations(people, client): + space, doc, owner, reader, outsider = people + added = upload(space, doc) + entry, = attachments.entries(added.meta) + url = reverse("spaces:page_attachments", args=[space.slug, doc.slug]) + download = local_download_url(space.slug, entry["path"]) + foreign = make_member("foreign", tenant="tenant:b") + SpaceMembership.objects.create(space=space, member=foreign, role="owner") + client.force_login(reader.user) + assert client.get(url).status_code == 200 + assert b"Add attachment" not in client.get(url).content + assert client.post(url, {"action": "remove"}).status_code == 403 + for member in (outsider, foreign): + client.force_login(member.user) + assert client.get(url).status_code == client.get(download).status_code == 404 + assert client.post(url, {"action": "add"}).status_code == 404 + SpaceMembership.objects.filter(space=space, member=reader).delete() + client.force_login(reader.user) + assert client.get(url).status_code == client.get(download).status_code == 404 + client.force_login(owner.user) + assert client.post(download).status_code == 405 + space.is_active = False + space.save() + assert client.get(download).status_code == 404 + client.logout() + assert client.get(url).status_code == client.get(download).status_code == 302 + + +def test_stale_missing_or_foreign_attachment_actions_do_not_change_files(people, client): + space, doc, owner, _, _ = people + added = upload(space, doc) + before = {str(p): p.read_bytes() for p in pageops.content_root().rglob('*') if p.is_file()} + for revision in (doc.revision, None, ""): + with pytest.raises(pageops.PageOpsError, match="changed"): + attachments.add(space.slug, doc.slug, SimpleUploadedFile("more.txt", b"more"), + revision=revision) + with pytest.raises(pageops.PageOpsError, match="not found"): + attachments.remove(space.slug, doc.slug, "assets/elsewhere.txt", revision=added.revision) + after = {str(p): p.read_bytes() for p in pageops.content_root().rglob('*') if p.is_file()} + assert after == before + guarded = Client(enforce_csrf_checks=True) + guarded.force_login(owner.user) + url = reverse("spaces:page_attachments", args=[space.slug, doc.slug]) + assert guarded.post(url, {"action": "remove"}).status_code == 403 + + +@pytest.mark.parametrize("path", ["../pages/notes.md", "nested/../../private", "..%2fsecret", + "missing", "link.txt", "folder/link.txt"]) +def test_download_refuses_missing_traversal_and_symlinks(people, client, tmp_path, path): + space, _, _, _, _ = people + secret = tmp_path / "secret.txt" + secret.write_text("private") + (pageops.assets_dir(space.slug) / "link.txt").symlink_to(secret) + (pageops.assets_dir(space.slug) / "folder").symlink_to(tmp_path, target_is_directory=True) + assert client.get(reverse("spaces:download", args=[space.slug, path])).status_code == 404 + + +def test_active_content_is_download_only_and_filenames_are_escaped(people, client): + space, doc, _, _, _ = people + added = upload(space, doc, '.html', b'') + entry, = attachments.entries(added.meta) + response = client.get(local_download_url(space.slug, entry["path"])) + assert response["Content-Disposition"].startswith("attachment;") + assert response["Content-Type"] == "application/octet-stream" + assert "sandbox" in response["Content-Security-Policy"] + assert b"".join(response.streaming_content) == b'' + menu = client.get(reverse("spaces:page_attachments", args=[space.slug, doc.slug])) + assert b"' + result = audit.review_html(audit.audit(data)) + assert b'' audit, rendered = audit_body(body) diff --git a/tests/test_source_date_supervisor.py b/tests/test_source_date_supervisor.py index 395b59b..13de889 100644 --- a/tests/test_source_date_supervisor.py +++ b/tests/test_source_date_supervisor.py @@ -341,9 +341,18 @@ def test_capsule_preparation_is_private_fresh_and_has_a_new_attempt_id(prepared, assert proofs[0] != proofs[1] +@pytest.fixture +def native_observer_tmp(): + # The native observer deliberately requires /tmp, regardless of TMPDIR. + import tempfile + + with tempfile.TemporaryDirectory(prefix="csoc-observer-test-", dir="/tmp") as directory: + yield Path(directory) + + @pytest.mark.parametrize("changed_image", [False, True]) def test_observer_prepares_without_capturing_or_refuses_missing_release( - prepared, tmp_path, monkeypatch, changed_image): + prepared, native_observer_tmp, monkeypatch, changed_image): import runpy import sys @@ -354,7 +363,7 @@ def test_observer_prepares_without_capturing_or_refuses_missing_release( finally: sys.path.pop(0) monkeypatch.setattr(supervisor.os, "geteuid", lambda: 0) # simulated root context only - output = tmp_path / "observed-capsule" + output = native_observer_tmp / "observed-capsule" if changed_image: node.dep["spec"]["template"]["spec"]["containers"][0]["image"] += "0" with pytest.raises(RuntimeError, match="prerequisite_missing"): diff --git a/tests/usecases/test_attachments_browser.py b/tests/usecases/test_attachments_browser.py new file mode 100644 index 0000000..06fabec --- /dev/null +++ b/tests/usecases/test_attachments_browser.py @@ -0,0 +1,36 @@ +"""Synthetic attachment menu journey through the real application UI.""" + +import subprocess +from pathlib import Path + +import pytest + +from coulomb_social.apps.spaces.services import create_space, put_space_page, set_space_access +from tests.test_space_permissions import make_member + + +@pytest.mark.browser +@pytest.mark.django_db(transaction=True) +def test_attachment_upload_copy_move_download_and_detach_through_ui(tmp_path, settings, request): + settings.DEBUG = True + settings.OIDC_ENABLED = False + settings.USER_ENGINE_BASE_URL = "" + settings.FLEX_AUTH_BASE_URL = "" + settings.FORGEJO_BASE_URL = "" + settings.CONTENT_ROOT = str(tmp_path / "content") + owner, reader, outsider = [make_member(n, tenant="tenant:attachment-test") + for n in ("file-author", "file-reader", "file-outsider")] + for member in (owner, reader, outsider): + member.issuer = "https://local.dev/issuer" + member.save() + space = create_space(owner, title="Shared attachments", slug="attachments-shared") + create_space(owner, title="Private destination", slug="attachments-private") + put_space_page(owner, space, "notes", title="Notes", body="Synthetic notes") + set_space_access(owner, space, reader.pk, "viewer") + server = request.getfixturevalue("live_server") + root = Path(__file__).resolve().parents[2] + result = subprocess.run( + ["node", str(root / "scripts/test-attachments-browser.mjs"), str(server)], + cwd=root, capture_output=True, text=True, timeout=120, + ) + assert result.returncode == 0, result.stdout + result.stderr diff --git a/workplans/CSOC-WP-0012-durable-recovery-service-readiness.md b/workplans/CSOC-WP-0012-durable-recovery-service-readiness.md index eb49779..ad4ca6d 100644 --- a/workplans/CSOC-WP-0012-durable-recovery-service-readiness.md +++ b/workplans/CSOC-WP-0012-durable-recovery-service-readiness.md @@ -18,7 +18,7 @@ Residual acceptance from CSOC-WP-0008, split at the user's request. Existing imp ## Current execution map — 2026-10-31 -Completed: T01 independent custody, T02 recovery, T05 dual-destination proof and T06 recurring source/rehearsal. T04 owns retention/capacity; T07 the pending attended route; T08 inactive installation; T09 native operational proof; T10 schedule activation/observation. T11/T12 expose the previously buried service-contract work. T03 remains the final service acceptance gate and is not an additional implementation estimate on top of T07–T12. +Completed: T01 independent custody, T02 recovery, T05 dual-destination proof, T06 recurring source/rehearsal, T07 the independently verified attended route and T08 inactive installation. T04's local operating capacity profile is admitted; retention and historical-copy disposition remain. T09 owns native operational proof; T10 schedule activation/observation. T11/T12 expose the previously buried service-contract work. T03 remains the final service acceptance gate and is not an additional implementation estimate on top of T07–T12. The [four-workplan assessment](../docs/release/goal-progress-audit-2026-10-31.md) records the scope split and effort estimate. Task counts are not effort percentages. @@ -558,7 +558,7 @@ status: progress priority: high state_hub_task_id: "a36722a5-5f13-5728-9d58-24f030c2bb00" needs_human: true -intervention_note: "Eight-entry completed-evidence custody repair is prepared and native preflight passes; founder terminal command is in docs/release/backup-completed-evidence-custody-review-2026-10-31.md, proposal fa2763d1dcbe92fe638a29c9a7a7d51c03d30849c4232f2d43f81ef705bae611. No change executed. Historical pair disposition and retention/capacity admission remain open; existing destinations and schedule are settled." +intervention_note: "Eight-entry historical custody repair and inactive installation are founder-executed and independently verified under T15/T08. The fixed local capacity profile was admitted through INSTALL8f442bf0. Provider retention/expiry and historical pair disposition remain open. Existing destinations and schedule are settled." ``` Remaining scope from T01. Close with owner-admitted capacity/headroom limits, a current generation/expiry plan, and explicit retention or deletion disposition for historical private production-pair copies. Execute only admitted expiry and retain native receipts. Proposed limits and a deletion candidate list are not admission. Evidence: `docs/release/backup-capacity-review-2026-10-30.md` and `generation-retention-post-date-2026-10-31.json`. No deletion has occurred. Existing RPF-WP-0053 custody/expiry ownership remains canonical. @@ -587,6 +587,22 @@ helper ownership exceptions. T04 still owns the exact historical eight-entry rep retention/capacity admission and historical-copy disposition. Source correction does not execute that repair or establish native recurring-service acceptance. +2026-10-31 operator-handoff audit: fresh native preflight again verifies all eight +entries and terminal historical operations with proposalfa2763d1 unchanged; its +execution receipt is absent. The existing human gate now links pending decision +3ec2f98e-9db3-47d9-bc1a-0387f980b81c, which State Hub automatically escalated to +human review. It is not approval or a changed scope. The founder has been given +the one-line command and exact SEAL response. After the return, independently +verify durable receipt and custody before progressing to installation. Evidence: +`docs/release/operator-handoff-check-2026-10-31.json`. + +Founder execution subsequently completed at16:36:38Z. Independent readback at +16:37:46Z verifies all eight intended metadata changes, unchanged hashes/paths/ +inodes/mtimes, terminal historical operations and whole-state custody at11,259,904 +bytes. The custody decision is resolved from this actual execution. Completed +native milestone T15 now holds that requirement; do not replay the seal command. +T04 remains progress for retention/capacity and historical-copy disposition. + ## T05 — Verify both approved backup alert destinations ```task @@ -613,23 +629,38 @@ Durable independent channel attempts, retry/replay, failed/pending/stale episode ```task id: CSOC-WP-0012-T07 -status: wait -wait_kind: human -blocking_reason: "Exact reviewed route is prepared; founder terminal execution and its durable verified receipt are pending." +status: done +decision_id: "5fa7234f-2a41-4eed-b4fc-9384f493572b" priority: high state_hub_task_id: "8c8e4810-d967-54b2-ae49-25d367eac459" -needs_human: true -intervention_note: "Existing attended route packet4d848106 awaits founder execution and independent verification. Destination/test-send approval is settled; do not resend the one-off proof." +needs_human: false ``` The exact owner execution packet is ready; durable receipt was absent at the 10:04:37Z observation. Founder execution and independent complete-resource/runtime-config postflight remain. Use the existing reviewed owner packet in `/home/worsch/rapp-telemetry/docs/coulomb-backup-route-execution-2026-10-31.md`, digest4d848106; inspect any existing attempt before further action. No renewed recipient approval is needed. Close only on durable verified native success, preserving the existing PVC and unrelated routes. +Fresh operator-handoff audit again finds no route receipt; the exact native +preflight passes with execution digest4d848106 unchanged. The previously unrouted +human gate now links pending decision5fa7234f-2a41-4eed-b4fc-9384f493572b, automatically +escalated by State Hub for human review. Existing destinations and schedule stay +settled. This fixes decision-queue visibility; it is not deployment, permission to +send another proof email or monitoring activation. The installation receipt is +also absent. Evidence: `docs/release/operator-handoff-check-2026-10-31.json`. + +2026-10-31 completed return: founder executed unchanged proposal4d848106 at +17:17:13Z–17:17:31Z. Independent postflight at17:19:36Z reads the durable +receipt and original execution packet and verifies revision6, all87resources, +actual Alertmanager configuration, unchanged image and preserved Grafana PVC. +T07 is done; do not replay the deployment. No test alert, capture or timer +activation occurred. Evidence: `backup-route-independent-return-2026-10-31.json`. +Earlier pending observations above are historical. + ## T08 — Finish the supervised source installer and install inactive units ```task id: CSOC-WP-0012-T08 -status: wait +status: done priority: high +needs_human: false state_hub_task_id: "ca41b874-5e09-538e-9692-d31d990d679b" depends_on: - CSOC-WP-0012-T04 @@ -640,18 +671,29 @@ The installation/rollback library and final supervised wrapper are implemented. The wrapper binds sources, archive/manifest, native preimages, both exclusive locks, full owner route verification, capacity/profile, SDK/custody and inactive/ disabled/no-queued-job guards. Explicit rollback retains preimages and refuses -interference. Current native preflight reaches and refuses the capacity guard's -historical custody exceptions; T04 now records that exact blocker. T07's route -receipt is also still absent. No installation attempt or target change occurred. +interference. The earlier native preflight refused the capacity guard's historical +custody exceptions; T15 now records their completed repair and independent +verification. T07's route execution is still awaiting its return. No installation +attempt or target change occurred. Then obtain the attended return and independently verify installed hashes and disabled units. Review: docs/release/backup-source-installation-execution-review-2026-10-31.md; source/gate evidence: backup-supervised-installer-validation-2026-10-31.json. -Preparation is complete; task status is now wait for the T04 custody/profile -return and T07 attended route. The exact eight-entry custody repair has a passing -native preflight but has not executed. Installation and independent postflight -still prevent this task from being done. T07 likewise waits for its founder run; -neither task represents a currently running native job. +The later content image release (RAPPS-WP-0015-T07, packetc9c32316) must follow +this installation: it refreshes the snapshot image pin that this reviewed bundle +currently fixes atdcc8dd2a. Both existing packet digests are preserved. Technical +sequencing decision: de5a4528-0864-4769-b9d0-2f658b0310c6; this is not image +execution approval. + +2026-10-31 completed return: founder executed unchanged proposal8f442bf0 at +17:22:48Z–17:22:54Z, explicitly confirming its local capacity profile. Independent +postflight verifies the outer/nested durable receipts, all19target hashes and +journaled file identities, preserved helpers, all5units loaded/inactive with +timers disabled, no queued jobs, SDK/credential-reference custody, capacity and +the actual revision6 route. T08 is done; do not replay installation. Evidence: +`backup-installation-independent-return-2026-10-31.json`. No capture, email, +deletion or timer activation occurred. T04 retains provider retention/expiry +and historical-copy disposition; T09/T10 retain recurring-service proof. ## T09 — Prove native paired service and recurring failure reception @@ -659,10 +701,15 @@ neither task represents a currently running native job. id: CSOC-WP-0012-T09 status: todo priority: high +depends_on: +- CSOC-WP-0012-T04 +- CSOC-WP-0012-T07 +- CSOC-WP-0012-T08 +- RAPPS-WP-0015-T07 state_hub_task_id: "c30bc200-6e0c-5651-b53a-be7a6f3dd207" ``` -After T07/T08 and applicable T04 operating admission, prove a native paired service capture, exact primary/mirror receipt, independent retrieval, received failed/pending/stale events in both channels, retry and restart continuity, and outside-producer failure detection. Retain correlated receipts without claiming an Alertmanager handoff is recipient delivery. Simulated source tests and the one-off email cannot close this task. +After T07/T08, applicable T04 operating admission and RAPPS-WP-0015-T07 content image delivery, prove a native paired service capture, exact primary/mirror receipt, independent retrieval, received failed/pending/stale events in both channels, retry and restart continuity, and outside-producer failure detection. Retain correlated receipts without claiming an Alertmanager handoff is recipient delivery. Simulated source tests and the one-off email cannot close this task. ## T10 — Activate and observe the approved recurring schedule @@ -670,33 +717,69 @@ After T07/T08 and applicable T04 operating admission, prove a native paired serv id: CSOC-WP-0012-T10 status: todo priority: high +depends_on: +- CSOC-WP-0012-T04 +- CSOC-WP-0012-T07 +- CSOC-WP-0012-T08 +- RAPPS-WP-0015-T07 +- CSOC-WP-0012-T09 state_hub_task_id: "009c9b51-32d4-5a35-a771-7cff0059c042" ``` -After T04 and T07–T09, activate the already approved 03:15 UTC schedule with five-minute jitter, RPO24h/stale26h and attended RTO60min target. Verify native timer/service state and subsequent scheduled capture/freshness evidence. Record measured recovery limits; cached-image rehearsal is not cold-start recovery. Do not ask the user to approve the schedule again. Close on observed operation, not merely enabling a timer. +After T04, T07–T09 and RAPPS-WP-0015-T07, activate the already approved 03:15 UTC schedule with five-minute jitter, RPO24h/stale26h and attended RTO60min target. Verify native timer/service state and subsequent scheduled capture/freshness evidence. Record measured recovery limits; cached-image rehearsal is not cold-start recovery. Do not ask the user to approve the schedule again. Close on observed operation, not merely enabling a timer. ## T11 — Close service registration, provenance, cost and smoke evidence ```task id: CSOC-WP-0012-T11 -status: todo +status: progress priority: high state_hub_task_id: "61f5f196-40e4-5901-a10e-318304809a00" ``` -Remaining T03 service-contract §§1–5 evidence: authenticated ops registration, canonical client × app × instance finance mapping and current ingestion/report, Forgejo successor provenance and governing identity-specialization decision, current SLO evidence, and CSOC-IN-0004 stage-1 smoke reconciliation. Consume existing identity/cohort returns from WP-0014/0015 without duplicating owner implementations. Use `docs/release/service-contract-conformance-2026-10-27.md` and `service-provenance-reconciliation-2026-10-30.md`. Resource requests and planning labor are not booked costs. +Remaining T03 service-contract §§1–5 evidence: authenticated ops registration, canonical client × app × instance finance mapping and current ingestion/report, current SLO evidence, and CSOC-IN-0004 stage-1 smoke reconciliation. T14 separately closes the Forgejo successor and existing identity-decision provenance bindings; that source/evidence milestone does not close this operational acceptance task. Consume existing identity/cohort returns from WP-0014/0015 without duplicating owner implementations. Use `docs/release/service-contract-conformance-2026-10-27.md` and `service-provenance-reconciliation-2026-10-30.md`. Resource requests and planning labor are not booked costs. + +2026-10-31 current telemetry return: read-only native Prometheus queries find +one current available Coulomb replica and 20,160 readiness samples over seven +days (observed minimum0/maximum1), but no Coulomb scrape target or probe/HTTP +request metric families among the inspected names. Actual retention is10days/ +10GiB. Readiness samples do not measure HTTPS availability or authenticated-page +latency, and no monthly window/percentile attainment is established. The concrete +remaining measurement work is application-level signals and retained evidence +for the stated windows, through the existing telemetry owner; no new exporter, +monitor, target, retention setting or public exposure was deployed. Evidence: +`docs/release/existing-telemetry-2026-10-31.json`. The conformance table now reflects +the already verified revision19 release and packaged client export, instead of +leaving revision18/source-only export as its lead state. T11 remains progress. ## T12 — Agree and prove complete client offboarding and expiry ```task id: CSOC-WP-0012-T12 -status: todo +status: progress priority: high state_hub_task_id: "54af74b0-3212-537e-a02c-d2d8a091caf9" ``` Remaining T03 service-contract §6: resolve external/unattributed data, obtain private delivery/acceptance of complete client export, and agree instance teardown, sessions, logs and primary/mirror/protected-kit expiry. Current packaged export proof covers 85 selected files and four record sets; it is not offboarding acceptance. Coordinate retention with T04 and personal portability with WP-0013-T03. Record non-destructive review first; do not infer authorization to delete data. +2026-10-31: T12 advances to progress with the concrete non-destructive scope review +in `docs/release/client-offboarding-scope-review-2026-10-31.md`. Fresh read-only +application checks repeat the accepted tenant's four record counts/85local files/ +zero external bindings, but find one other space with an external binding under +the deployment default tenant. The accepted export therefore does not establish +whole-instance client scope. Forty-three session rows remain unattributed by +this count-only check. Native storage metadata confirms the2Gi content PVC is +Helm-retained even though its PV reclaim policy is Delete. Five retained recovery +databases are running on internal networks with no published ports; exact private +volume/path inventory and existing receipt bindings are retained. None was stopped +or deleted. The review maps SQL/content, sessions, external/shared owners, node +journals, local copies and primary/mirror/protected stores to their required +returns. §6.2 requires an offboarding capability, not destruction of this active +service as a completion shortcut. Scope/retention agreement, disposable procedure +proof and private export delivery/acceptance remain open. Aggregate evidence: +`docs/release/client-offboarding-scope-2026-10-31.json`. + ## Completion Finish when every task above has its stated evidence and acceptance, or hand off any newly discovered residual to a live work record first. Cross-owner dependencies remain their existing canonical records. @@ -730,3 +813,54 @@ check includes protected browser journeys. No production operation, frozen histo capsule or pending repair/installation/route packet was changed. Future reviewed capsules carry these sources; the existing eight-entry repair remains pending T04, and native recurring evidence remains T09. This task closes source prevention only. + + +## T14 — Reconcile Forgejo successor and identity decision provenance + +```task +id: CSOC-WP-0012-T14 +status: done +priority: medium +needs_human: false +state_hub_task_id: "77c11ad0-3271-563a-9114-750b1e1cee16" +``` + +Completed2026-10-31. Extracted from T11 at its verified boundary, without counting +T11 as another completed outcome. Resolved founder migration/registry decisions, +active freeze canon, completed RAIL-HO-WP-0005 and its independently read-back +Hub state establish Forgejo as the successor to the contract's historical Gitea +address. Fresh TLS/version and read-only deployment metadata match the current +revision19 source/image records; no legacy teardown permission is inferred. + +Existing founder decision8bbce691 explicitly approves Coulomb's NetKingdom +OIDC/PKCE client restoration and app release; it supplies the recorded link for +ADR-0001's authentication choice under service-contract§8.3. Technical decision +7b68c574-aa61-4517-bb31-1ecda554c3e8 binds this evidence without new admission or +claims about all ADR statements, demand triggers, or the pending reset-role grant. +Evidence: docs/release/service-provenance-binding-2026-10-31.json and the latest +service-provenance reconciliation. T11 remains progress for registration, finance, +current SLO and stage-1 smoke; no runtime, credentials, cost facts or acceptance +were fabricated or changed by this audit. + +## T15 — Seal completed historical backup evidence custody + +```task +id: CSOC-WP-0012-T15 +status: done +priority: high +needs_human: false +decision_id: "3ec2f98e-9db3-47d9-bc1a-0387f980b81c" +state_hub_task_id: "4c1ee823-c148-5c8c-8fea-0ab8c3a4213e" +``` + +Completed2026-10-31. This extracts the verified historical custody requirement +from T04; its retention/capacity/copy-disposition scope remains live there. +Founder executed exact proposalfa2763d1 at16:36:38Z. The durable private receipt +and independent native readback verify root/private ownership and modes for all +eight reviewed entries with original hashes, bytes, paths, device/inodes and +mtimes preserved. Historical operations remain terminal, their helper is absent, +the four operation locks are available, no overlapping hostPath mount exists, +and the whole-state custody guard passes at11,259,904bytes. No capture, data +deletion, application write or timer activation occurred. Do not rerun the seal +command. Evidence: +`docs/release/backup-completed-evidence-custody-return-2026-10-31.json`. diff --git a/workplans/CSOC-WP-0013-sharing-content-hardening.md b/workplans/CSOC-WP-0013-sharing-content-hardening.md index 83b915d..2cb0223 100644 --- a/workplans/CSOC-WP-0013-sharing-content-hardening.md +++ b/workplans/CSOC-WP-0013-sharing-content-hardening.md @@ -8,7 +8,7 @@ status: active owner: codex topic_slug: coulomb-social created: "2026-10-08" -updated: "2026-10-31" +updated: "2026-11-01" origin: residual origin_ref: CSOC-WP-0008 state_hub_workstream_id: "70370f4e-7cba-5b0a-853b-fedcd2bd49a5" @@ -16,14 +16,44 @@ state_hub_workstream_id: "70370f4e-7cba-5b0a-853b-fedcd2bd49a5" Residual acceptance from CSOC-WP-0008, split at the user's request. Existing implementation/release/acceptance evidence remains in the originating workplan; these tasks own the remaining concrete outputs. Source checks never grant runtime or productive acceptance. -## Current execution map — 2026-10-31 +## Current execution map — 2026-11-01 -T01 storage enforcement is complete. T04 now exposes completed fidelity-review preparation. T02 still owns actual corpus/assets fidelity verdicts and final content acceptance; its revision lifecycle detail is T05. T03 owns four separate UC-003/004/005/006 productive verdicts. No review verdict has been inferred from tests or weekly-call acceptance. +T01 storage enforcement and T04 fidelity-review preparation are complete. +Necessary WP-0012-T07 route and T08 inactive source installation are founder- +executed and independently verified, including capacity admission. Full recurring +backup/service acceptance and migration/cutover remain in their existing plans. + +The combined content release is deployed at revision20; its editor follow-up is +now independently verified at revision21/imageb58d858a/source31eaa58. Full desired +spec/source/helper checks pass,97business rows/98files are unchanged, and43owner +views/43outsider denials pass. The live editor check renders all43editors and +verifies42saved-file download links. No execution retry is required. Evidence: +editor-attachments-{receipt,independent-postflight,live-editor}-2026-11-01.json. + +T02 retains final content acceptance: three custom-topic rendering cases are +accepted for now (decision69ec9380), two original attachments are recovered, +and all four reviewed scrap cases are intentionally omitted as obsolete legacy +data (decision6db14274). No further scrap capture/review is required. Other source +body/graph reconciliation remains WP-0014; none is silently waived here. + +T05 retention policy is accepted (decisionc3a191f2), and the interrupted-operation +fix is released with crash/recovery proof. No purge cutoff is authorized. +T06 author-managed attachments are deployed. The user confirmed “Both tabs now +show the attachment,” accepting the reported editor-visibility correction. +The user accepts step2: copy/edit/remove independence, move preservation and +owner access through the old address. Participant denial remains for step3. + +T03 owns the separate UC-003/004/005/006 productive outcomes in the +[acceptance session](../docs/usecases/productive-content-acceptance-session.md). +Sharing/old-address denial uses nominated `testuser` after sign-in; only one +active member was found at the last check. The owner portion of copy/move is accepted; incoming-address denial, +sharing/revocation and account-local portability verdicts remain open. Narrow visibility acceptance +and automated checks do not supply broader workflow verdicts. The [four-workplan assessment](../docs/release/goal-progress-audit-2026-10-31.md) -records the scope split and effort estimate. Task counts are not effort percentages. -Historical notes retain the evidence trail; this map and the task blocks govern -current status. No original acceptance requirement has been dropped. +retains the scope split. Historical task notes retain earlier preparation and +release evidence; this map and the latest task returns govern current status. +No original acceptance requirement has been dropped. ## T01 — Finish unambiguous storage enforcement @@ -127,6 +157,9 @@ T01 is done. Productive corpus/sharing/portability verdicts remain T02/T03. id: CSOC-WP-0013-T02 status: progress priority: high +needs_human: false +decision_id: "6db14274-4450-4ae2-b77a-cd4c88369eaa" +intervention_note: "Source-resource disposition is settled: two attachments recovered and all four reviewed scraps explicitly omitted as obsolete legacy data by Bernd. No further scrap review is required. Combined release and productive acceptance remain separately required." state_hub_task_id: "8d06cdee-e0ce-5c3e-bf98-705deed0ee62" ``` @@ -166,11 +199,94 @@ of incorrectly describing storage hardening as unreleased. Source/capture proof: docs/release/bubble-source-continuation-2026-10-31.json. Productive UC-003–006 verdicts and remaining content/revision obligations are still open. +2026-10-31 body-resource check: 18 distinct captured link entries and all 28 +occurrences preserve exact reference attributes through the current product +renderer. Verification binds 242 source bodies and 236 prepared article pages; +no remote link was fetched. The compiler now explicitly retains and blocks four +deferred scrap relation fields in addition to the one attachment flag. This +narrows lossiness uncertainty but does not resolve those records or accept the six +markup-change cases, original Bubble appearance or productive content fidelity. +Evidence: `docs/release/bubble-body-resources-2026-10-31.json`. T02 remains progress. + +The subsequent bounded native read recovered fresh unchanged metadata for all +five resource-affected articles, but no resource records. The visible resource +control is Add attachment, not a demonstrated path to existing content, and was +left untouched. The operator viewing/download-path question is pending under +WP-0014-T01. `docs/release/bubble-resource-capture-2026-10-31.json` records the +incomplete return and private review; neither resource absence nor fidelity is +inferred. T02 remains progress. + +2026-10-31 closure follow-through: audit found a concrete numbering defect: +sanitization removed15ordered-list start attributes across4captured records. +The bounded renderer correction permits ol.start while preserving event-handler, +custom-tag and unsafe-URL denial. All650full-gate tests pass, including browser +journeys. Refreshed private cards verify all242bodies unchanged and restore every +start value; structural sanitizer cases fall from6to3, all custom topic fields. +Three missing bodies and unresolved resource flags remain explicit. Evidence: +`docs/release/ordered-list-fidelity-2026-10-31.json`; private review is +`~/.config/coulomb-social/auth/exports/rendering-review-20261031-numbering/index.html`. +This source correction still requires packaged release and productive verdicts. +It neither changes the original capture nor accepts the remaining custom fields. + +2026-10-31 user return: “This is fine for now” accepts the displayed rendering of +the three custom-topic cases in the refreshed numbering review. Decision +69ec9380-e5f6-41aa-af88-f697fc1c8a66 records that narrow disposition. The user +suggested the original visual overlay may expose existing resources and requested +a plain action menu in the replacement app; no original resource was waived. + +The user-suggested visual-overlay check visited all5resource-affected pages and +hovered42visible images. Its download link targets an already retained Visual; +no typed scrap/attachment resource was found. No resource absence is inferred. +Private evidence is resources-hover-20261031 alongside the original review; +public receipt: docs/release/bubble-resource-hover-2026-10-31.json. The exact +viewing-path or per-case disposition question remains pending. + +2026-10-31 confirmed modal follow-through: the user identified Add attachment as +both the add and download modal. Opening it and using Open file recovered two +original files, one on the retained flagged page and one on the user-supplied +example outside the original245article capture. Each UI download exactly matches +an independent CDN fetch;2relations,2new attachment child records and original +creation/modification timestamps are retained privately. All5previously flagged +parent bodies/dates remain unchanged. The known parent draft now carries its +attachment and date provenance; its body and existing metadata remain exact. +The example stays separately retained pending source reconciliation. The4scrap +relations remain unresolved; their empty attachment modals do not prove that +scraps are absent. No source mutation, original-evidence overwrite, production +import or productive verdict occurred. Capture and private preparation evidence: +docs/release/bubble-attachment-recovery-2026-10-31.json. Private downloads/review: +~/.config/coulomb-social/auth/exports/attachment-bundle-20261031/attachments-index.html. +All670full-gate checks and7synthetic resource guards pass. This clears the unknown +attachment-modal navigation path; it does not waive the remaining source gaps. + +2026-10-31 resumed audit: each of the4scrap-bearing pages opens its navigator, +but none yields a typed resource record or a scrap control. The raw deferred +references do not point to the parent or another captured article. This does not +prove absence or obsolescence. A four-case private review now isolates the needed +dispositions, under pending decision6db14274-4450-4ae2-b77a-cd4c88369eaa: +~/.config/coulomb-social/auth/exports/scrap-review-20261031/index.html. +Public receipt: docs/release/bubble-scrap-navigation-2026-10-31.json. The additional +example/attachment records are not referenced by the original reconciliation, +so they cannot silently resolve its prior source graph exceptions. The exact +image proposal still has no durable execution receipt; no live release or +productive acceptance is inferred. + +2026-10-31 explicit user disposition: “Lets ignore the scrap cases as they are +legacy relicts that are no longer needed.” All four reviewed cases are accepted +intentional omissions, under resolved decision6db14274-4450-4ae2-b77a-cd4c88369eaa. +Their obsolete scrap content is outside migration/content acceptance scope; no +further scrap capture or verdict is required. The exact four-case manifest and +raw-reference hashes bind this disposition. Original source evidence, existing +page bodies/dates and the two recovered attachments are retained. Other source +exceptions, release delivery and productive acceptance are not waived. Public +receipt: docs/release/bubble-scrap-disposition-2026-10-31.json; exact private +mapping: ~/.config/coulomb-social/auth/exports/scrap-review-20261031/disposition.json. +The earlier unresolved-scrap notes are historical and superseded by this return. + ## T03 — Review productive sharing and personal portability ```task id: CSOC-WP-0013-T03 -status: todo +status: progress priority: high state_hub_task_id: "8edd7661-17b0-5aa0-b177-6170aa159623" ``` @@ -181,6 +297,20 @@ Review owner Share space/revocation, reader/outsider denial, account-local expor Prepared exact UC-003–006 steps in docs/usecases/sharing-portability-review.md; no acceptance inferred. +2026-10-31: Revision20 is independently verified and the user requested productive +acceptance now. The focused session and sample attachments are prepared; owner +authoring/attachments review has been requested. A second participant must sign +in after deployment for ungranted/viewer/revoked checks. No verdict is inferred. + +2026-11-01 actual step2 return: “The step 2 of the walkthrough seems fine.” +Record UC-004 owner workflow accepted: copy/edit/detach preserves the original, +move retains content/Visual/downloads, and the saved old address reaches the +moved page for its owner. This also accepts the owner copy/removal portion of +T06 and supports T05's productive lifecycle evidence. Incoming-address denial +for a participant lacking destination access remains explicitly part of step3; +UC-004's permission outcome, UC-005 and UC-006 stay open. Evidence: +`docs/release/productive-copy-move-return-2026-11-01.json`. + ## T04 — Prepare private corpus fidelity review evidence ```task @@ -216,6 +346,104 @@ release delivery and productive review. User accepted the retain-all/manual-cuto production purge, image deployment or user acceptance is inferred. The current verified release remains revision19/image522bcd7f until a new owner return. +The corrected source f306dbf is published as immutable candidate +`sha256:d3f2f5bb90f0ba79f4e1d8098d2eb06f8940b8789e5ecb851914bbab364597ec`. +All80 packaged source files match; runtime package/Python versions are unchanged, +and no new migration is present. The previous image reproduces four interrupted- +attachment failures; the candidate passes all29 lifecycle checks in a read-only, +network-disabled container without an application overlay. Full make check passes +546 tests, including required browsers, with zero failures/errors/skips. Build, +provenance and paired baseline/candidate test receipts are under +`docs/release/content-lifecycle-*-2026-10-31.json`. RAPPS-WP-0015-T07 now owns the +new exact release preparation/execution return. T05 remains progress until that +return and productive acceptance; policy acceptance is settled and no live purge +is authorized. No production image or release pin changed during this work. + +Exact owner image packetc9c32316 is now prepared and RAPPS-WP-0015-T07 waits on +WP-0012-T08. Its40controller cases pass; the separate values/snapshot paths retain +historical capsules. Current native image preview is image-only at revision19. +Candidate and rollback images preserve the retained13tables/86files and render40 +owner pages/deny40outsiders. Live93-row baseline includes a new session and login +timestamps since the92-row retained point; content/migrations match. The execution +must capture a new exact paired point before Helm. Native full preflight refuses +missing installation8f442bf0 before creating an execution journal. Existing backup +packets retain their digests; image delivery follows inactive installation and +precedes native recurring-service acceptance/activation. Details and later commands +are in docs/usecases/sharing-portability-review.md. No image release or productive +acceptance has occurred; T05 remains progress. + +## T06 — Deliver author-managed attachments through a plain action menu + +```task +id: CSOC-WP-0013-T06 +status: progress +priority: high +needs_human: false +decision_id: "0d538348-aadd-41a2-88f6-191defcf22b2" +intervention_note: "Founder executed2cddcf38; revision21/imageb58d858a and complete unchanged-data/source/helper checks are independently verified. Execution gate is complete; productive editor recheck has been requested." +state_hub_task_id: "ff59b908-2d56-5fa5-b1b9-556aee1bae14" +``` + +User requested a sensible action menu instead of a visual overlay, explicitly +including author add/remove. Provide authenticated downloads for existing local +resources, bounded uploads, revisioned attachment associations, and space-level +read/write permissions. Removal detaches the current association while retaining +bytes needed by old revisions. Copies and moves preserve independent attachments +and incoming-address permissions. Preserve page body and original source dates. + +Source implementation and synthetic browser journey pass. Main full gate670 and +isolated release gate568 pass; all110 focused packaged checks pass with no app +source overlay. Source1139a6d is published as immutable image7906f461. The release +combines attachment actions, ordered-list starts and copy/move crash recovery. +Evidence: docs/release/attachment-menu-validation-2026-10-31.json and +content-acceptance-{build,provenance,tests}-2026-10-31.json. Packaged recovery/export checks now preserve all13tables/86files and verify +40owner/40outsider views plus85export files. The refreshed exact proposal is +e96752ecfa7adef2fd1d9da48902c8e63de124ccfd272fe20ba79b758b3ca4a3; review is +docs/release/content-acceptance-review-2026-10-31.md. Full native preflight passes at unchanged revision19/data fingerprints. Founder +execution is requested under decisiond9b70cc5-e035-40ae-8d0d-020190581202. Attended +release/independent return and productive user verdict remain required. Existing unresolved Bubble resources +remain T02/WP-0014-T01; this UI does not establish their recovery or completeness. + +2026-10-31 productive review feedback: Bernd reports that the workflow seems fine +mostly, but attachments visible on the original page are absent from the edit +tabs. This is a concrete T06 UI gap, not attachment loss or a completed verdict. +The editor currently lacks the attachment list; save/rename retain associations. +The correction displays saved attachments and downloads in the editor, opens +management in another tab, and retains the list plus submitted text after a +stale-edit refusal. The synthetic browser reproduces the two-editor scenario. +Main670, isolated568 and packaged110 gates pass. Candidate31eaa58/imageb58d858a +passes recovered-store views/export. Exact proposal2cddcf38 passes full native +preflight at revision20/97rows/98files; existing93rows/86files are unchanged. +Founder execution is requested under decision0d538348-aadd-41a2-88f6-191defcf22b2. +RAPPS-WP-0015-T08 tracks delivery. Revision20 remains live until verified return; +user acceptance remains open. + +2026-11-01 founder return: exact proposal2cddcf38 completed at23:32:38Z on +2026-10-31 (00:32 local), Helm revision21/imageb58d858a. Independent complete-spec, +source/helper, sole writer and data verification agrees with the durable receipt: +97business rows/98files unchanged,43owner pages and43outsider denials pass. +Decision0d538348 is resolved; no rerun is required. A focused productive recheck +of both editor tabs and downloads is requested. Fresh-point mirror/restore/export +and source-pin reconciliation are verified:13tables/119rows/98files restored; +all97selected-client files export with source SQL/files unchanged. RAPPS-WP-0015-T08 +is done. Recovery is stopped, timers inactive/disabled; user verdicts stay separate. + +2026-11-01 productive return: “Both tabs now show the attachment” confirms the +reported editor-visibility gap is resolved after revision21. This is the actual +user verdict for that narrow fix. Copy/edit/remove independence and move behavior +are the next requested checks; old-address denial still requires `testuser`. +Downloads/removal/sharing/portability are not inferred from the visibility return. + ## Completion Finish when every task above has its stated evidence and acceptance, or hand off any newly discovered residual to a live work record first. Cross-owner dependencies remain their existing canonical records. + +Current closure boundary after revision20: RAPPS-WP-0015-T07 delivery is complete, +including the fresh-point recovery/export and source-pin reconciliation. T02, +T05 and T06 await their productive outcomes in the T03 session. `testuser` is the +user-nominated sharing participant; its sign-in/member ID and the independent +authoring, attachment, copy/move, sharing and account-portability verdicts remain +open. The deployed source passed the670-test main gate,568-test isolated release +gate and110packaged checks before deployment; the subsequent snapshot-pin +reconciliation passed all9snapshot/restore regressions. The editor attachment display correction is now verified at revision21; +productive acceptance is still required. diff --git a/workplans/CSOC-WP-0014-cohort-transfer-bubble-offramp.md b/workplans/CSOC-WP-0014-cohort-transfer-bubble-offramp.md index 87baca8..4b1c3a4 100644 --- a/workplans/CSOC-WP-0014-cohort-transfer-bubble-offramp.md +++ b/workplans/CSOC-WP-0014-cohort-transfer-bubble-offramp.md @@ -18,7 +18,7 @@ Residual acceptance from CSOC-WP-0008, split at the user's request. Existing imp ## Current execution map — 2026-10-31 -T05 records completed capture/tooling preparation; T04 now closes the independently verified accepted-cohort date correction. T01 still owns source exceptions, currentness and identity/grant/assets reconciliation. Remaining date verification is T06 and actual further-cohort migration is T07. T02 productive journey/transfer verdicts and T03 the separate cutover decision remain open. Bubble login renewal is currently cleared; four source graph exceptions and one unassigned record remain. +T05 records completed capture/tooling preparation; T08 adds verified private Visual capture and draft-tree materialization. T04 now closes the independently verified accepted-cohort date correction. T01 still owns source exceptions, currentness and identity/grant/assets reconciliation. Remaining date verification is T06 and actual further-cohort migration is T07. T02 productive journey/transfer verdicts and T03 the separate cutover decision remain open. Bubble login renewal is currently cleared; four source graph exceptions and one unassigned record remain. The [four-workplan assessment](../docs/release/goal-progress-audit-2026-10-31.md) records the scope split and effort estimate. Task counts are not effort percentages. @@ -32,7 +32,7 @@ id: CSOC-WP-0014-T01 status: progress priority: high needs_human: true -intervention_note: "Renewed Bubble session verified and targeted capture completed on 2026-10-31. Four source graph exception groups still need original content/date evidence or explicit disposition; private review: ~/.config/coulomb-social/auth/exports/source-exception-review-20261031/index.html. No further login renewal is currently required; no capture job is running." +intervention_note: "Fresh source session verified on 2026-10-31. Four source graph exception groups still need original content/date evidence or explicit disposition: ~/.config/coulomb-social/auth/exports/source-exception-review-20261031/index.html. The user-confirmed Add attachment modal yielded two verified files; private review is ~/.config/coulomb-social/auth/exports/attachment-bundle-20261031/attachments-index.html. The four reviewed scrap relations are explicitly omitted as obsolete legacy data under resolved decision6db14274; they no longer require capture or review. The additional example and two attachment child records need source reconciliation. Bounded capture is terminal; no further login renewal is currently required." state_hub_task_id: "0002882d-f156-5a5d-b524-d04793f4684d" ``` @@ -116,6 +116,95 @@ body and new grants have not been inferred. No productive import occurred. Evidence: docs/release/bubble-source-continuation-2026-10-31.json and its source manifest. T01 remains progress; the login-renewal gate is cleared. +2026-10-31 access reconciliation: a hash-bound offline inventory now separates +observed, missing, null, empty and unresolved access fields across all 70 fresh +space records and 245 article records; the retained 71st space remains explicitly +inventory-only. Source references normalize to three distinct principals. Nine +Created By references cannot be normalized; absent membership fields remain +unknown rather than empty grants. Space privacy and article visibility each have +two observed values, whose authorization semantics are not established by this +capture. The October 6 accepted import admits tegwick as owner of ReichelAG only; +it does not establish a general source-account or role mapping for other spaces. +Private per-record review: `~/.config/coulomb-social/auth/exports/access-review-20261031/index.html`. +Aggregate proof: `docs/release/bubble-access-audit-2026-10-31.json`. No target +identity mapping, grant proposal or import manifest was inferred. T01 remains +progress until source exceptions and the actual identity/grant/assets/link +reconciliation are resolved; no extra completion task was added for this audit. + +2026-10-31 body-resource follow-through: all 18 distinct captured body-link entries +(28 occurrences on one article) retain exact reference attributes through the +current product Markdown/sanitizer path. All 242 captured bodies and 236 prepared +article pages match source bytes; the compiler reproduces all 545 original draft +files unchanged. Destination availability and original Bubble appearance were not +tested. Four `_list_custom_scrap` fields contain deferred LOOKUP strings rather +than captured record lists. Preparation now retains these values privately and +marks them as unresolved, including an explicit affected-space blocker. They are +separate from the one attachment-flagged article. The immutable original and Visual +bundles remain untouched; the supplemental private report records the new blockers. +Proof: `docs/release/bubble-body-resources-2026-10-31.json`. Actual scrap/attachment +capture or disposition, identity/grants, source exceptions and productive transfer +remain open. T01 stays progress; link serialization proof is not a transfer verdict. + +2026-10-31 resource recovery attempt: 294 retained network documents contain no +typed scrap/attachment records. A fresh, bounded read-only visit of the five +affected articles returned all five parent records unchanged, but no typed +resources or empty-resource envelopes. There were 41 HTTP 200 source responses, +zero read timeouts and two response-read failures; this is not evidence that +resources do not exist. DOM inspection found no links to the four descriptor IDs. +After allowing the UI to settle on one scrap parent and the attachment parent, +the resource-related controls are Add attachment, which were not activated. +The observed read endpoints use encoded request bodies; no guessed lookup or +broader query was submitted. The operator has been asked where existing resources +can be viewed/downloaded, with all five original pages linked in the private +review above. No deletion, empty list or discontinuation inferred. Original +capture and Visual preparation are unchanged. Evidence: +`docs/release/bubble-resource-capture-2026-10-31.json`. T01 stays progress and the +existing source-resource gaps remain explicit until recovery or disposition. + +2026-10-31 confirmed modal follow-through: the user identified Add attachment as +both the add and download modal. Opening it and using Open file recovered two +original files, one on the retained flagged page and one on the user-supplied +example outside the original245article capture. Each UI download exactly matches +an independent CDN fetch;2relations,2new attachment child records and original +creation/modification timestamps are retained privately. All5previously flagged +parent bodies/dates remain unchanged. The known parent draft now carries its +attachment and date provenance; its body and existing metadata remain exact. +The example stays separately retained pending source reconciliation. The4scrap +relations remain unresolved; their empty attachment modals do not prove that +scraps are absent. No source mutation, original-evidence overwrite, production +import or productive verdict occurred. Capture and private preparation evidence: +docs/release/bubble-attachment-recovery-2026-10-31.json. Private downloads/review: +~/.config/coulomb-social/auth/exports/attachment-bundle-20261031/attachments-index.html. +All670full-gate checks and7synthetic resource guards pass. This clears the unknown +attachment-modal navigation path; it does not waive the remaining source gaps. + + +2026-10-31 resumed audit: each of the4scrap-bearing pages opens its navigator, +but none yields a typed resource record or a scrap control. The raw deferred +references do not point to the parent or another captured article. This does not +prove absence or obsolescence. A four-case private review now isolates the needed +dispositions, under pending decision6db14274-4450-4ae2-b77a-cd4c88369eaa: +~/.config/coulomb-social/auth/exports/scrap-review-20261031/index.html. +Public receipt: docs/release/bubble-scrap-navigation-2026-10-31.json. The additional +example/attachment records are not referenced by the original reconciliation, +so they cannot silently resolve its prior source graph exceptions. The exact +image proposal still has no durable execution receipt; no live release or +productive acceptance is inferred. + + +2026-10-31 explicit user disposition: “Lets ignore the scrap cases as they are +legacy relicts that are no longer needed.” All four reviewed cases are accepted +intentional omissions, under resolved decision6db14274-4450-4ae2-b77a-cd4c88369eaa. +Their obsolete scrap content is outside migration/content acceptance scope; no +further scrap capture or verdict is required. The exact four-case manifest and +raw-reference hashes bind this disposition. Original source evidence, existing +page bodies/dates and the two recovered attachments are retained. Other source +exceptions, release delivery and productive acceptance are not waived. Public +receipt: docs/release/bubble-scrap-disposition-2026-10-31.json; exact private +mapping: ~/.config/coulomb-social/auth/exports/scrap-review-20261031/disposition.json. +The earlier unresolved-scrap notes are historical and superseded by this return. + + ## T02 — Review remaining journeys and record transfer decisions ```task @@ -324,13 +413,98 @@ Remaining all-space scope split from T04. Preserve original space/page/chunk cre ```task id: CSOC-WP-0014-T07 -status: todo +status: progress priority: high state_hub_task_id: "26e9bbab-34cf-5616-94d4-61464027895c" ``` After each source/identity/grant/assets mapping is ready under T01 and content rules under WP-0013, prepare and execute the authorized stepwise all-space migration in the requested LIFO order with restartable per-space receipts. Verify content, original dates (T06), ownership, explicit grants, assets and links without overwriting later local edits. User productive transfer verdicts remain T02. All spaces are eventual scope; a draft tree is not a productive import. Cutover/retirement remains T03. +2026-10-31 preparation: the corpus command now has an opt-in private receipt +directory for new-space imports. Completed boundaries can be resumed without +reimporting completed spaces or overwriting their later edits/grants. Source, +ordered manifest, mapped member IDs, database configuration and content root are +bound to the checkpoint. An interrupted or uncertain space stops for explicit +reconciliation; the helper does not infer rollback from an exception. Real process +exit tests cover intent, committed SQL before receipt, and a completed boundary. +See [receipt review](../docs/release/corpus-import-receipts-2026-10-31.md). +This is offline implementation evidence. Source/identity/grant reconciliation, +owner executor integration with writer fencing and a fresh paired backup, native +deployment, per-cohort postflight and productive acceptance remain open. No further +source cohort was imported into production, and the existing reviewed executor +or release packets were not changed. + +2026-10-31 uncertain-import follow-through: receipt mode now persists a reserved +Space UUID before writes and creates it exclusively, preventing an existing or +concurrently created same-slug SQL row from being updated/adopted. The new +`inspect_content_corpus` command classifies matching reserved commits, partial +stores, later edits, foreign identities and legacy unbound receipts without +changing content, SQL or checkpoints. It compares original source dates when +present, uses existing locks and a consistent read transaction, and explicitly +does not authorize retry or claim an external writer fence. Fifteen new cases and +the extended real process-exit inspection pass. Full make check:629 passes with +required browsers and no skips. Disposable PostgreSQL16.14 corpus/receipt/inspection +tests also pass with60 actual read-only/repeatable-read mode checks; all disposable +resources were removed. Review: +[uncertain-import inspection](../docs/release/corpus-import-inspection-2026-10-31.md). +T07 remains progress: concrete state reconciliation, supervised owner integration, +source/grant admission, native release/import/postflight and productive verdicts +are still required. No live cohort or approved executor was changed. + +2026-10-31 matching-commit reconciliation: added an explicit operation that +completes one uncertain checkpoint only when its reserved identity, source files, +metadata and mapped grants match the exact reviewed inspection. It reacquires +existing locks, uses a read-only SQL snapshot, makes destination files durable, +rechecks state and atomically retains the original started unit and inspection +with the completed checkpoint. It changes no SQL/content and refuses partial, +foreign, changed, unbound or unattributed-staging states. Real process exits on +both sides of replacement retain inspectable evidence; resumed imports preserve +later edits/grants. Full make check passes 648 tests with required browsers and +zero failures/errors/skips. Isolated PostgreSQL 16.14 also passes, with 243 +read-only/repeatable-read transaction checks and disposable-resource cleanup. +Review: [matching-commit reconciliation](../docs/release/corpus-import-reconciliation-2026-10-31.md). +This advances preparation within T07, which remains progress. Other uncertain +states, owner integration and verified external fencing/paired backup, cohort +admission, native release/import and productive verdicts remain open. No live +checkpoint, source cohort, grant or frozen execution packet was changed. + ## Completion Finish when every task above has its stated evidence and acceptance, or hand off any newly discovered residual to a live work record first. Cross-owner dependencies remain their existing canonical records. + + +## T08 — Capture and prepare the retained Visual images + +```task +id: CSOC-WP-0014-T08 +status: done +priority: high +needs_human: false +state_hub_task_id: "b7464ad5-9087-54a9-b3da-991b87bcefc8" +``` + +Completed2026-10-31. The previous67-tree review bundle retained Visual URLs but +left its Visual fields empty. Captured242 references through108 distinct HTTPS +URLs on the single recorded Bubble CDN;90 distinct binary images are retained +privately (168,139,156 downloaded bytes, including repeated content). No cookies, +authorization headers, arbitrary origins or redirects are used. Private capture +has per-image/aggregate limits and a final completion receipt; partial captures +cannot be mistaken for completed output. No browser login renewal was needed. + +A separate new bundle links local images on236 eligible draft pages and their67 +space indexes. All71 spaces, unassigned records and original gates remain. An +independent verifier checks738 output files against manifests, preserves all545 +original files modulo303 Visual-only frontmatter changes, and proves unchanged +bodies, original dates and all other metadata. All90 images decode in an offline +browser. This does not establish original Bubble rendering fidelity, current source +completeness, grants, or productive acceptance. Blocked/unassigned Visuals remain +retained even when no draft destination exists. Original capture is unchanged. + +Evidence: docs/release/bubble-visual-preparation-2026-10-31.json; private bundle +~/.config/coulomb-social/auth/exports/visual-bundle-20261031.24focused checks and +full make check570pass, no failures/errors/skips; final verifier and Ruff pass. +This extracts completed Visual preservation from T01/T05 without closing the +broader mapping/transfer requirement. T01 retains18body links, one attachment +flag, four graph exception groups, one unassigned record, source currentness, +identity/explicit grants and destination reconciliation. T07 remains unstarted +for productive transfer; T02/WP-0013 retain productive fidelity/verdicts. diff --git a/workplans/CSOC-WP-0015-communication-runtime-acceptance.md b/workplans/CSOC-WP-0015-communication-runtime-acceptance.md index a1f0e86..080ca27 100644 --- a/workplans/CSOC-WP-0015-communication-runtime-acceptance.md +++ b/workplans/CSOC-WP-0015-communication-runtime-acceptance.md @@ -54,6 +54,7 @@ flow verdicts remain T04. See docs/release/acceptance-audit-2026-10-27.md. ```task id: CSOC-WP-0015-T02 status: progress +depends_on: [RPF-WP-0068-T04, EMAIL-WP-0010-T04] priority: high state_hub_task_id: "0604cb15-72c7-5d15-bb42-0335d4ec80df" ``` @@ -95,6 +96,58 @@ See docs/release/platform-mail-custody-preparation-2026-10-31.md for the owner review packet and exact closure conditions. This dependency is tracked as work, not left only in this task's progress narrative. +2026-10-31 sender-boundary follow-through: EMAIL-WP-0010 now owns the separate +supervised sender configuration transition. Its T01 provides a tested read-only +ledger observer and native return at15:16:41Z: all six tables,2accepted invitations, +121accepted verification sends,1suppression and no uncertain outcomes. The source +matches the packaged receiver. The observer correctly gives delivery receipts +precedence over retained in_flight invitation rows and uses one transaction so +concurrent WAL writes cannot split the result. No rows, passwords or mail were +printed or changed. This clears current-state investigation only; it is not a +writer fence or rollout approval. Owner T02 still requires the complete executor +and interrupted-execution/rollback proof; T03/T04 retain execution and native +return after RPF-WP-0068. Evidence: +[sender boundary review](../../email-connect/docs/sender-transition-boundary-2026-10-31.md). All existing +recipient, reply ownership and separate credential-admission decisions persist. + +The owner sender task EMAIL-WP-0010-T02 is now progress: its protocol core has +durable intent records, exact configuration changes and explicit recovery that +captures the current ledger instead of restoring an older one. All 27 new cases +pass, including 10 real process exits with modeled cluster effects and actual +SQLite/retry behavior. The owner suite reports 219 passed / 21 existing optional +harness skips. This does not establish native execution: adapter/custody/fencing, +helper lifecycle, runtime verification and the sealed attended packet remain open +in that same task. Evidence: +[protocol review](../../email-connect/docs/sender-transition-protocol-2026-10-31.md). + +Native adapter preparation now adds exact UID/spec changes, metadata-only Secret +queries, foundation receipt/projection checks, the shared capture lock, PVC writer +fencing, durable helper identities/cleanup and runtime denial checks. Five native +server dry-runs passed without changing production. The actual snapshot/replay +path passed in the pinned receiver image with synthetic data and no network; +the disposable container was removed. Thirty new cases pass; owner suite is now +249 passed / 21 existing optional harness skips. Evidence: +[native adapter review](../../email-connect/docs/sender-native-adapter-2026-10-31.md). +EMAIL-WP-0010-T02 remains progress pending the complete source-bound capsule, +attended entry point, assembled forward/recovery proof and exact review packet. +The separate owner credential admission, native sender execution/return and +productive inbox/bounce/user verdicts remain open. No grant, deployment change, +production capture or email send was performed by these checks. + +The owner executor now has its attended entry point and eight-file source-bound +capsule path. Twenty-one additional assembly cases pass with the actual coordinator, +protocol and native adapter against modeled Kubernetes, real SQLite subprocesses +and actual scoped WSGI runtime probes; owner suite is 270 passed / 21 existing +optional harness skips. EMAIL-WP-0010-T02 has moved to wait on RPF-WP-0068-T04: +sealing a live proposal requires the completed credential-owner handoff and its +successful foundation receipt. A real preparation invocation refused that missing +handoff before contacting the node or writing a capsule. After the return, fresh +native sealing and the exact review packet finish T02; execution/acceptance remain +T03/T04. Evidence: +[attended entry review](../../email-connect/docs/sender-attended-entry-2026-10-31.md). +This consumer task stays progress because sender/inbound/bounce and productive +acceptance obligations are still open. + ## T03 — Complete independent ledger recovery and copy custody ```task @@ -245,13 +298,68 @@ Acceptance scope split from T04: current Case B, UC-010 registration and UC-011 ```task id: CSOC-WP-0015-T06 -status: todo +status: progress +depends_on: [NK-WP-0054-T10] priority: high state_hub_task_id: "e17a6d09-1fdd-5fa1-a36d-763802e37f7f" ``` -Remaining owner admission from T04 is canonical NK-WP-0054-T05. Isolated native Authelia/LLDAP reset and full browser integration are proven, including read-only-bind refusal and successful scoped password replacement. Obtain and assess the owner native authority/runtime return; preserve MFA/TLS/directory boundaries and link acceptance here. Do not create a duplicate provisioning lane or infer a live grant from the synthetic proof. Evidence: `docs/release/native-recovery-browser-review-2026-10-31.md` and `native-password-reset-review-2026-10-30.md`. +Remaining owner admission from T04 is canonical NK-WP-0054-T10, the explicit reset follow-through to the completed T05 bind split. Isolated native Authelia/LLDAP reset and full browser integration are proven, including read-only-bind refusal and successful scoped password replacement. Obtain and assess the owner native authority/runtime return; preserve MFA/TLS/directory boundaries and link acceptance here. Do not create a duplicate provisioning lane or infer a live grant from the synthetic proof. Evidence: `docs/release/native-recovery-browser-review-2026-10-31.md` and `native-password-reset-review-2026-10-30.md`. + +2026-10-31 owner-return assessment: NK-WP-0054-T05 is now done and its founder +bind migration plus restore drill have been inspected. They prove sign-in/MFA, +not post-split password change. Current native ConfigMap uses svc-authelia; both +Ready native images match our isolated proof, and the dated owner receipt assigns +strict-readonly. This establishes an unclosed reset-authority requirement, not a +claimed production reset attempt. NK-WP-0054-T09 records the completed assessment; +T10 now owns supported reset authority, preparation, explicit admission, native +return and recovery verification in the same owner workplan. T06 advanced to +progress for this assessment and consumes T10; it cannot close on T05's done label. +Evidence: docs/release/reset-owner-return-assessment-2026-10-31.json. No credential, +mail, password, directory grant or live configuration was changed. Prior personal +reset acceptance is preserved with its original date and scope. + +2026-10-31 authority preparation: owner T11 records the native role matrix; +T10 is progress. Password-manager permits ordinary/application-admin/non-admin +service password changes but refuses LLDAP administrators. Seven other directory +writes per role refuse with valid admin controls. Ten direct cases and eight +Authelia flows pass; all containers removed. Full gate546 and original reset +regression pass. Detailed scope and next admission conditions are in +native-password-reset-review-2026-10-30.md and reset-authority-validation-2026-10-31.json. +A complete runtime proposal still needs current subject classification and, if +that subject is an LLDAP administrator, its supported recovery route. T06 remains progress, with no live +grant, reset or productive acceptance inferred. + +2026-10-31 observation preparation: the retained personal acceptance identifies +tegwick. Technical decision8770d4ea binds the current-role check to that exact +account and makes administrator recovery conditional on its actual role, preserving +the original contract. Owner T12 is done: a fixed read-only observer passes92 +unit/transport checks and native LLDAP classification/metadata-preservation checks. +T13 waits for the human admin's private terminal authentication and credential-free +JSON return. The concrete command and limits are in the native-reset review; +reset-observer-preparation-2026-10-31.json binds sources/tests. T06 remains progress; +no actual directory-role observation, password change or grant is claimed. + +2026-10-31 attended role return: founder supplied the12:57:44Z observation. +Independent private-file and source-hash verification passes; owner T13 is done. +svc-authelia has only strict-readonly, while the accepted tegwick subject is not +an LLDAP administrator (one other group, name not disclosed). This resolves the +classification dependency. T10 can prepare the reviewed password-manager transition; +its broad non-admin reach remains part of admission. No directory-admin recovery +route is needed for this accepted flow on the observed roles, and no new policy, +password change or productive reset acceptance is inferred. Evidence: +reset-role-observation-2026-10-31.json and reset-role-return-verification-2026-10-31.json. ## Completion Finish when every task above has its stated evidence and acceptance, or hand off any newly discovered residual to a live work record first. Cross-owner dependencies remain their existing canonical records. + + +2026-10-31 reset transition preparation: owner T14 prepares the source-bound, +crash-recoverable role controller; T15 explicitly holds founder admission and +execution; T16 holds source alignment, productive reset/app/MFA and identity +backup/restore acceptance. The same running Authelia process picks up grant and +rollback in the confined native fixture. Membership rollback does not revoke +HTTP tokens issued during a grant; the review includes their up-to-24-hour scope. +No production role/password changed. T06 remains progress and consumes owner T10, +whose acceptance is preserved rather than closed by fixture proof.