diff --git a/CURRENT_STATE.md b/CURRENT_STATE.md index e8d9413..f98cf46 100644 --- a/CURRENT_STATE.md +++ b/CURRENT_STATE.md @@ -1,13 +1,60 @@ --- name: synaplex current state description: Front door for the synaplex.ai system — publication + evaluation lab + operational pipeline. Read first every session. -updated: 2026-07-27T00:12Z (Cadence lineage, dependency closure, and GitHub controls verified) +updated: 2026-07-27T00:33Z (Cadence route identity and inbox service hardening verified) owner: executive (principal: evan) phase: artifact-delivery-instrument-v2 BLOCKED_PRE_ENTRY and quarantined; artifact-coherence-transfer-v1 INVALIDATED by Codex; public projection v1.3.0 live with five archived lineage artifacts; zero findings --- # synaplex — current state +## Cadence route identity and inbox hardening — complete (2026-07-27) + +The final lifecycle projection is deployed at +`https://14154a06.synaplex.pages.dev` and `https://synaplex.ai`. Fresh +route-specific GETs to `/lineage/cadence/` on both origins returned identical +5187-byte bodies with byte digest +`sha256:f8a57cebbd817977e1929d977d474b2c564dae025ba08560de07f079f3cda4e8`. +Both bodies contain the title `Cadence · synaplex`, canonical +`https://synaplex.ai/lineage/cadence/`, five `Cadence` strings, and the exact +“Not a Claim. Not Evidence. Not a finding. Not a current study.” boundary. +This supersedes the earlier propagation-edge observation where an HTTP 200 +temporarily carried homepage identity; status alone is not the success +criterion. + +The final Tailwind 4/Preflight visual comparison was rerun from the built site. +Home and AI Native lineage retain their exact pre-migration dimensions, +background, font stack, and 100.8px headline. Mean absolute RGB deltas remain +0.0220 and 0.0113 on a 0–255 scale, with changed-channel ratios 0.000245 and +0.000198. The rejected no-Preflight candidate caused the earlier roughly +20–28% delta and taller pages; it was never shipped. Screenshot digests, +normalized metrics, and the Cadence identity render are recorded in +`docs/dependency-migration-2026-07.md`. + +`synaplex-inbox.service` is now installed and running under the dedicated +`synaplex-inbox` identity with no capabilities or ambient capabilities, +`NoNewPrivileges`, strict read-only system protection, private temp/devices, +kernel and control-group protection, namespace/SUID restrictions, and +localhost-only cgroup IP policy. The server needs no host write path: +`ReadWritePaths` is empty and the inbox is explicit in `ReadOnlyPaths`. +An exact-unit canary returned the same nonce-gated 4354-byte payload as the +old service, digest +`sha256:caff845d611110d2e6f1116263f864258f5393a6a861bb5266d50c753d37a5af`; +the installed unit repeated that witness and listens only on +`127.0.0.1:8088`. The runtime containment audit reports zero findings and +`systemd-analyze security` improved from `9.6 UNSAFE` to `1.3 OK`. +Independent executive/operator verification reproduced the effective controls, +reported `NRestarts=0`, obtained the same 200/4354-byte nonce-scoped response +and digest, and separately confirmed the central auditor's zero findings. + +The previous unit is retained mode 0600 at +`/opt/workspace/runtime/.meta/synaplex-inbox.service.before-hardening-2026-07-27T00-27-39Z` +for bounded rollback. The only dated exception is `PrivateNetwork=true`, +because a separate loopback namespace would break the required host-loopback +route; the fixed loopback bind plus default-deny/localhost-only IP policy is +the compensating control. Full canary, rollback, install, and outcome details +are in `docs/inbox-service-hardening-2026-07.md`. + ## Cadence lineage and dependency closure — complete (2026-07-27) PR [#16](https://github.com/evanfollis/synaplex/pull/16) merged as @@ -34,7 +81,7 @@ The public projection is v1.3.0 with digest `sha256:4ae99babff63e92c607978f80e18a89c235868fd91b2bbf1a21c104945585e98` and counts 3 research / 0 findings / 3 mechanisms / 3 engineering cases / 18 sources / 4 conjectures / 5 lineage records. It is deployed from the exact -merge at `https://9e4b3c3f.synaplex.pages.dev` and +final lifecycle state at `https://14154a06.synaplex.pages.dev` and `https://synaplex.ai`. Clean external GETs returned 200 for the roots, `/lineage/cadence/`, and `/knowledge/public-projection.json`. Immutable and apex projection bytes are identical with byte digest diff --git a/deploy/synaplex-inbox.service b/deploy/synaplex-inbox.service new file mode 100644 index 0000000..d7902fc --- /dev/null +++ b/deploy/synaplex-inbox.service @@ -0,0 +1,51 @@ +[Unit] +Description=Synaplex nonce-gated inbox server +After=network.target + +[Service] +Type=simple +User=synaplex-inbox +Group=synaplex-inbox +WorkingDirectory=/opt/workspace/runtime/inbox +Environment=PYTHONDONTWRITEBYTECODE=1 +ExecStart=/usr/bin/python3 /opt/workspace/supervisor/scripts/lib/inbox-server.py +Restart=on-failure +RestartSec=5s +UMask=0077 +MemoryMax=128M +TasksMax=16 + +# The server reads static inbox material and requires no host write path. +ReadOnlyPaths=/opt/workspace/runtime/inbox +NoNewPrivileges=true +CapabilityBoundingSet= +AmbientCapabilities= +PrivateTmp=true +PrivateDevices=true +ProtectSystem=strict +ProtectHome=true +ProtectClock=true +ProtectHostname=true +ProtectKernelLogs=true +ProtectKernelTunables=true +ProtectKernelModules=true +ProtectControlGroups=true +ProtectProc=invisible +ProcSubset=pid +LockPersonality=true +MemoryDenyWriteExecute=true +RemoveIPC=true +RestrictNamespaces=true +RestrictRealtime=true +RestrictSUIDSGID=true +RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6 +SystemCallArchitectures=native +SystemCallFilter=@system-service +SystemCallErrorNumber=EPERM + +# Host-loopback reachability is required; cgroup egress remains localhost-only. +IPAddressDeny=any +IPAddressAllow=localhost + +[Install] +WantedBy=multi-user.target diff --git a/docs/dependency-migration-2026-07.md b/docs/dependency-migration-2026-07.md index cff2c24..91ad56e 100644 --- a/docs/dependency-migration-2026-07.md +++ b/docs/dependency-migration-2026-07.md @@ -68,3 +68,32 @@ Both retained `rgb(242, 240, 233)` as the root background and the same body font stack; the lineage headline remained 100.8px. The new `/lineage/cadence/` route was separately rendered and inspected, including its “Not a Claim. Not Evidence. Not a finding. Not a current study.” boundary. + +The first Tailwind 4 conversion attempt omitted Preflight. That candidate was +not shipped: it changed the homepage height from 2950px to 3374px, changed the +lineage height from 2032px to 2277px, and produced roughly 20–28% changed RGB +channels through browser-default list, heading, and typography styles. Enabling +the Tailwind 4 import (and therefore Preflight) restored the established reset. +No intentional visual redesign is part of this migration. + +The comparison was rerun from the final build on 2026-07-27. The results were +identical to the accepted comparison above: homepage screenshot +`sha256:cd44df1402834dec32d0fe074dfdfca8c422550f32b8c9347b442d638c8c9095` +remained 1440x2950 with mean absolute RGB delta 0.0220 (0.0000863 when +normalized to 0–1) and changed-channel ratio 0.000245 against the retained +pre-migration image +`sha256:91a1eeffa1e2682cb0c6dcf8eb2c4b8f52f3986a7b6aeca06c1b5a90a64d25f1`. +The AI Native lineage screenshot +`sha256:e3d8f35ac8879c08229fcc5cb388435127dc02a5135b6acf442ad34460af78cc` +remained 1440x2032 with mean absolute RGB delta 0.0113 (0.0000442 normalized) +and changed-channel ratio 0.000198 against +`sha256:0712a705c6e9abf105421d315e6c315d16a7678bd8e8c158589a58ead3642341`. +Computed styles again reported the original background, font stack, and +100.8px headline. Visual inspection found no typography, spacing, list, or +layout reset. + +The final Cadence screenshot is +`sha256:a06afaa784018eb2860b0bb7df25a53e202bddcbe45c763d822b8fa14ec16106`. +Its rendered identity was checked separately: title `Cadence · synaplex`, +canonical `https://synaplex.ai/lineage/cadence/`, `Cadence` H1, and the explicit +epistemic boundary were all present. diff --git a/docs/inbox-service-hardening-2026-07.md b/docs/inbox-service-hardening-2026-07.md new file mode 100644 index 0000000..7bc0f40 --- /dev/null +++ b/docs/inbox-service-hardening-2026-07.md @@ -0,0 +1,86 @@ +# Inbox service hardening — 2026-07-27 + +This is an operational hardening receipt, not scientific Evidence. It records +the canary, rollback boundary, installed controls, and observed outcome for +`synaplex-inbox.service`. + +## Required access + +The server reads the nonce-gated static tree at +`/opt/workspace/runtime/inbox` and the existing Python launcher at +`/opt/workspace/supervisor/scripts/lib/inbox-server.py`. It binds only +`127.0.0.1:8088`. It requires no host write path, so the unit declares the +inbox tree with `ReadOnlyPaths` and deliberately has no `ReadWritePaths` +allowance. Its isolated private `/tmp` remains available to the process. + +The installed process uses the dedicated `synaplex-inbox:synaplex-inbox` +system identity rather than root. Before canary startup, that identity was +verified to traverse and read the inbox and launcher paths. + +## Mandatory controls + +The versioned unit enables: + +- `NoNewPrivileges=true`, empty capability and ambient-capability sets, and + `RestrictSUIDSGID=true`; +- `ProtectSystem=strict`, `ProtectHome=true`, owner-only `UMask=0077`, and the + explicit read-only inbox path; +- `PrivateTmp=true`, `PrivateDevices=true`, and process visibility protection; +- kernel tunable, module, log, clock, hostname, and control-group protection; +- namespace, realtime, personality, architecture, and system-call restrictions; +- default-deny IP policy with localhost as the only allowed destination and + only `AF_UNIX`, `AF_INET`, and `AF_INET6` address families. + +The repository deployment-contract check asserts these controls and rejects +adding a host `ReadWritePaths` allowance to this read-only service. + +## Canary, installation, and rollback + +The previous root-running unit is retained with mode 0600 at: + +`/opt/workspace/runtime/.meta/synaplex-inbox.service.before-hardening-2026-07-27T00-27-39Z` + +Its SHA-256 digest is +`4a5c832f5b3d2a781039ede74a7ce1a5d7c561a13b1f2528c96d6f66325915dc`. +Rollback is the bounded operation of installing that file back to +`/etc/systemd/system/synaplex-inbox.service`, running +`systemctl daemon-reload`, restarting the service, and repeating the +nonce-gated payload check. + +The exact candidate unit was first installed under the transient canary unit +name. The production unit was stopped, the canary was started on the real +loopback port, and a nonce-gated GET returned 4354 bytes with SHA-256 +`caff845d611110d2e6f1116263f864258f5393a6a861bb5266d50c753d37a5af`, +identical to the pre-canary payload. Effective canary properties showed the +dedicated identity, strict filesystem protection, private temp/devices, empty +capabilities, all required kernel and namespace controls, no host write paths, +and localhost-only IP policy. `systemd-analyze security` rated it `1.3 OK`. +The canary was then removed and the original service was successfully restored +before permanent installation. + +Permanent installation used an automatic failure trap that would restore the +retained unit on any restart, payload, or control-check failure. No rollback +was triggered. The installed and repository unit bytes are identical at +`sha256:4fcc085a87f58145b4710625e8b1cc1b2b7491b887b1297c349848f835711acd`. +The service is enabled and has remained active since +2026-07-27T00:29:26Z. It listens only on `127.0.0.1:8088`; its post-install +nonce-gated response is the same 4354-byte payload with the same digest. +The workspace runtime containment audit reports zero findings at every +severity, and the installed unit retains the `1.3 OK` systemd exposure rating +(improved from `9.6 UNSAFE`). + +Independent executive/operator verification then confirmed the effective +dedicated identity, empty capability set, strict filesystem protection, +`NoNewPrivileges`, private temp/devices, `ActiveState=active`, and +`NRestarts=0`. Its separate nonce-scoped GET returned HTTP 200 with the same +4354 bytes and payload digest, and the central auditor independently reported +zero findings for the unit. + +## Smallest dated exception + +2026-07-27: `PrivateNetwork=true` is not enabled because it would place the +server in a separate loopback namespace and break its required host-loopback +consumer route. This exception does not relax any other sandbox control. +Compensating controls are the launcher's fixed `127.0.0.1` bind, +`IPAddressDeny=any`, `IPAddressAllow=localhost`, and the restricted address +families. The observed listener is only `127.0.0.1:8088`. diff --git a/scripts/check_deploy_contract.py b/scripts/check_deploy_contract.py index 59d3994..c9ac1ce 100644 --- a/scripts/check_deploy_contract.py +++ b/scripts/check_deploy_contract.py @@ -13,6 +13,27 @@ "PrivateTmp=true", ) +INBOX_REQUIRED = ( + "User=synaplex-inbox", + "Group=synaplex-inbox", + "UMask=0077", + "ReadOnlyPaths=/opt/workspace/runtime/inbox", + "NoNewPrivileges=true", + "CapabilityBoundingSet=\n", + "AmbientCapabilities=\n", + "PrivateTmp=true", + "PrivateDevices=true", + "ProtectSystem=strict", + "ProtectKernelTunables=true", + "ProtectKernelModules=true", + "ProtectControlGroups=true", + "LockPersonality=true", + "RestrictNamespaces=true", + "RestrictSUIDSGID=true", + "IPAddressDeny=any", + "IPAddressAllow=localhost", +) + def main() -> None: units = sorted((REPO_ROOT / "deploy").glob("*-v2.service")) @@ -26,9 +47,23 @@ def main() -> None: errors.append(f"{unit.name}: missing {requirement}") if "ExecStart=/bin/bash" not in text: errors.append(f"{unit.name}: launcher must resolve configured paths") + inbox = REPO_ROOT / "deploy" / "synaplex-inbox.service" + if not inbox.is_file(): + errors.append("synaplex-inbox.service: missing versioned source") + else: + text = inbox.read_text(encoding="utf-8") + for requirement in INBOX_REQUIRED: + if requirement not in text: + errors.append( + f"synaplex-inbox.service: missing {requirement.rstrip()}" + ) + if "ReadWritePaths=" in text: + errors.append( + "synaplex-inbox.service: read-only server must not gain a host write path" + ) if errors: raise SystemExit("\n".join(errors)) - print(f"deployment contract: clean ({len(units)} units)") + print(f"deployment contract: clean ({len(units)} v2 units + inbox)") if __name__ == "__main__":