Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -107,3 +107,11 @@ GULOGULO_MAIL_MAX_CONNECTIONS_PER_IP=20
GULOGULO_MAIL_MAX_MESSAGES_PER_USER_PER_MINUTE=60
GULOGULO_MAIL_QUEUE_MAX_ATTEMPTS=5
GULOGULO_MAIL_QUEUE_RETRY_BASE_MS=60000

# LP6 is a private, synthetic backup/restore/retention/DR proof. The smoke
# harness generates LP6_TEST_KEY_B64 at runtime; never commit a real key.
GULOGULO_LP6_VOLUMES_EXTERNAL=false
GULOGULO_LP6_NETWORK=gulogulo-lp6-runtime
GULOGULO_LP6_COMPOSE_PLATFORM=linux/amd64
LP6_TEST_KEY_B64=
LP6_KEY_REFERENCE=kms/local-synthetic/lp6-v1
52 changes: 49 additions & 3 deletions .github/workflows/quality-gates.yml
Original file line number Diff line number Diff line change
Expand Up @@ -94,11 +94,13 @@ jobs:
test -f doc/lp3-local-mail.md
test -f doc/lp4-local-web.md
test -f doc/lp5-local-operations-capacity.md
test -f doc/lp6-local-backup-dr.md
test -f release/v1-release-evidence.template.json
test -f release/lp2-local-services.json
test -f release/lp3-local-mail.json
test -f release/lp4-local-web.json
test -f release/lp5-local-operations-capacity.json
test -f release/lp6-local-backup-dr.json
test -f scripts/m10-release-audit.mjs
test -f scripts/lp2-compose-audit.mjs
test -f scripts/lp2-compose-smoke.mjs
Expand All @@ -115,6 +117,22 @@ jobs:
test -f scripts/lp5-capacity-smoke.ts
test -f scripts/lp5-proof-check.ts
test -f tsconfig.lp5.json
test -f scripts/lp6-source-fixture.ts
test -f scripts/lp6-backup-worker.ts
test -f scripts/lp6-restore-worker.ts
test -f scripts/lp6-compose-audit.ts
test -f scripts/lp6-compose-smoke.ts
test -f tsconfig.lp6.json
test -f src/backup/backup-contract.ts
test -f src/backup/backup-contract.test.ts
test -f src/backup/index.ts
test -f src/backup/backup-contract.mjs
test -f src/lifecycle/retention.ts
test -f src/lifecycle/retention.test.ts
test -f src/lifecycle/retention.mjs
test -f src/lifecycle/account-lifecycle.ts
test -f src/lifecycle/account-lifecycle.test.ts
test -f src/lifecycle/account-lifecycle.mjs
test -f src/capacity/capacity-contract.ts
test -f src/capacity/capacity-contract.test.ts
test -f src/ops/patch/status.ts
Expand Down Expand Up @@ -332,6 +350,11 @@ jobs:
scripts/lp5-compose-smoke.ts \
scripts/lp5-capacity-smoke.ts \
scripts/lp5-proof-check.ts \
scripts/lp6-source-fixture.ts \
scripts/lp6-backup-worker.ts \
scripts/lp6-restore-worker.ts \
scripts/lp6-compose-audit.ts \
scripts/lp6-compose-smoke.ts \
scripts/m10-release-audit.mjs \
.gitignore \
.github/workflows/commit-tests.yml \
Expand Down Expand Up @@ -447,9 +470,13 @@ jobs:
grep -Fq 'REPLAYED_CODE' src/auth/totp.ts
grep -Fq 'challenge' src/auth/webauthn.ts
grep -Fq 'One-time recovery' src/auth/recovery-codes.ts
grep -Fq 'DEFAULT_TRASH_RETENTION_DAYS = 28' src/lifecycle/retention.mjs
grep -Fq "BACKUP_ENCRYPTION_ALGORITHM = 'aes-256-gcm'" src/backup/backup-contract.mjs
grep -Fq 'PURGE_REJECTED' src/lifecycle/retention.mjs
# LP6 canonicals live in TypeScript; the MJS files are compatibility
# bridges and must only re-export the typed implementation.
grep -Fq 'DEFAULT_TRASH_RETENTION_DAYS = 28' src/lifecycle/retention.ts
grep -Fq "BACKUP_ENCRYPTION_ALGORITHM = 'aes-256-gcm'" src/backup/backup-contract.ts
grep -Fq 'PURGE_REJECTED' src/lifecycle/retention.ts
grep -Fq "export * from './retention.ts'" src/lifecycle/retention.mjs
grep -Fq "export * from './backup-contract.ts'" src/backup/backup-contract.mjs
grep -Fq 'createLogRotationPolicy' src/observability/log-policy.ts
grep -Fq 'createStructuredEvent' src/observability/structured-event.ts
grep -Fq 'createAlertPolicy' src/observability/alert-policy.ts
Expand All @@ -468,6 +495,14 @@ jobs:
grep -Fq 'profiles: ["lp5"]' compose.yaml
grep -Fq 'profiles: ["lp5-check"]' compose.yaml
grep -Fq 'lp5-patch-state:/var/lib/gulogulo/patch:ro' compose.yaml
grep -Fq 'gulogulo-lp6-backup' compose.yaml
grep -Fq 'profiles: ["lp6"]' compose.yaml
grep -Fq 'profiles: ["lp6-check"]' compose.yaml
grep -Fq 'lp6-backup-data:/var/lib/gulogulo/lp6-backup:ro' compose.yaml
grep -Fq "BACKUP_ENCRYPTION_ALGORITHM = 'aes-256-gcm'" src/backup/backup-contract.ts
grep -Fq "export * from './backup-contract.ts'" src/backup/backup-contract.mjs
grep -Fq 'DEFAULT_TRASH_RETENTION_DAYS = 28' src/lifecycle/retention.ts
grep -Fq "export * from './retention.ts'" src/lifecycle/retention.mjs
grep -Fq 'MIGRATION_PHASES' src/upgrade/compatibility.mjs
grep -Fq 'createSchemaMigrationPlan' src/upgrade/compatibility.mjs
grep -Fq 'createUpgradeController' src/upgrade/control-plane.mjs
Expand Down Expand Up @@ -515,6 +550,7 @@ jobs:
node -e "const t=require('./release/lp3-local-mail.json'); if (t.spdxLicenseIdentifier !== 'MIT' || t.author !== 'Sythos (https://www.sythos.net)' || t.milestone !== 'LP3' || t.proofType !== 'local_synthetic_mail' || t.networkPolicy !== 'offline_dependencies' || t.internalNetwork !== true || t.enableIpv6 !== true || t.ipFamilies?.join(',') !== 'ipv4,ipv6' || t.syntheticDataOnly !== true || t.publicDnsRequired !== false || t.publicAcmeEnabled !== false || t.externalDeliveryEnabled !== false || t.hostPortsPublished !== false || t.dockerSocketMounted !== false || t.policy?.catchAll !== false || t.policy?.automaticForwarding !== false || t.policy?.scanFailureMode !== 'fail_closed' || t.policy?.trashRetentionDays !== 28 || t.protocols?.imap?.idle !== true || t.protocols?.lmtp?.quotaReservationBeforeAck !== true || t.protocols?.sieve?.redirect !== false) process.exit(1)"
node -e "const t=require('./release/lp4-local-web.json'); if (t.spdxLicenseIdentifier !== 'MIT' || t.author !== 'Sythos (https://www.sythos.net)' || t.milestone !== 'LP4' || t.proofType !== 'local_synthetic_web_dav' || t.networkPolicy !== 'offline_dependencies' || t.internalNetwork !== true || t.enableIpv6 !== true || t.ipFamilies?.join(',') !== 'ipv4,ipv6' || t.syntheticDataOnly !== true || t.publicDnsRequired !== false || t.publicAcmeEnabled !== false || t.hostPortsPublished !== false || t.dockerSocketMounted !== false || t.credentialsCommitted !== false || t.web?.sameOriginOnly !== true || t.session?.csrfOnAuthenticatedMutations !== true || t.dav?.tenantBoundEtags !== true || t.dav?.tenantBoundSyncTokens !== true || t.dav?.masterContentAccess !== false || t.discovery?.tenantBound !== true || t.architectureValidation?.defaultWorkflowMode !== 'amd64' || t.architectureValidation?.finalWorkflowMode !== 'multiarch' || t.architectureValidation?.finalModePlatforms?.join(',') !== 'linux/arm64' || t.architectureValidation?.arm64RequiredBeforeMergeOrRelease !== true || t.architectureValidation?.composeProofPlatform !== 'linux/amd64') process.exit(1)"
node -e "const t=require('./release/lp5-local-operations-capacity.json'); if (t.spdxLicenseIdentifier !== 'MIT' || t.author !== 'Sythos (https://www.sythos.net)' || t.milestone !== 'LP5' || t.proofType !== 'local_synthetic_operations_capacity' || t.networkPolicy !== 'offline_dependencies' || t.internalNetwork !== true || t.enableIpv6 !== true || t.ipFamilies?.join(',') !== 'ipv4,ipv6' || t.hostPortsPublished !== false || t.dockerSocketMounted !== false || t.capacity?.claim !== 'bounded_local_proof_only' || t.capacity?.amd64Budget?.activeIdleConnections !== 8 || t.architectureValidation?.defaultWorkflowMode !== 'amd64' || t.architectureValidation?.finalWorkflowMode !== 'multiarch' || t.architectureValidation?.finalModePlatforms?.join(',') !== 'linux/arm64' || t.architectureValidation?.arm64RequiredBeforeMergeOrRelease !== true) process.exit(1)"
node -e "const t=require('./release/lp6-local-backup-dr.json'); if (t.spdxLicenseIdentifier !== 'MIT' || t.author !== 'Sythos (https://www.sythos.net)' || t.milestone !== 'LP6' || t.proofType !== 'local_synthetic_backup_restore_retention_dr' || t.networkPolicy !== 'offline_dependencies' || t.internalNetwork !== true || t.enableIpv6 !== true || t.ipFamilies?.join(',') !== 'ipv4,ipv6' || t.syntheticDataOnly !== true || t.hostPortsPublished !== false || t.dockerSocketMounted !== false || t.recovery?.metadataEncryption !== 'aes-256-gcm' || t.recovery?.checksumAlgorithm !== 'sha256' || t.recovery?.retentionDays !== 28 || t.recovery?.rpoMinutes !== 15 || t.recovery?.rtoMinutes !== 60 || t.recovery?.failedRestorePreservesSource !== true || t.recovery?.holdsPreventPurge !== true || t.recovery?.idempotentPurge !== true || t.architectureValidation?.defaultWorkflowMode !== 'amd64' || t.architectureValidation?.composeProofPlatform !== 'linux/amd64' || t.architectureValidation?.finalWorkflowMode !== 'multiarch' || t.architectureValidation?.finalModePlatforms?.join(',') !== 'linux/arm64' || t.architectureValidation?.arm64RequiredBeforeMergeOrRelease !== true) process.exit(1)"
bash -n docker/lp1-network/entrypoint-ca.sh
bash -n docker/lp1-network/entrypoint-dns.sh
bash -n docker/lp2-tls/entrypoint-tls.sh
Expand Down Expand Up @@ -690,6 +726,11 @@ jobs:
shell: bash
run: npm run test:lp5

- name: Run LP6 static and typed backup/restore/DR gates
if: inputs.architecture_mode != 'multiarch' && hashFiles('scripts/lp6-compose-audit.ts') != '' && hashFiles('src/backup/backup-contract.ts') != '' && hashFiles('src/lifecycle/retention.ts') != ''
shell: bash
run: npm run test:lp6

# In the default amd64 mode, the Compose proofs run on the amd64 GitHub
# runner before any architecture image work that could be expensive.
- name: Run LP3 local mail Compose proof (amd64)
Expand All @@ -709,6 +750,11 @@ jobs:
shell: bash
run: npm run test:lp5:docker

- name: Run LP6 local backup/restore/DR Compose proof (amd64)
if: inputs.architecture_mode != 'multiarch' && hashFiles('scripts/lp6-compose-smoke.ts') != '' && hashFiles('compose.yaml') != '' && hashFiles('src/backup/backup-contract.ts') != '' && hashFiles('src/lifecycle/retention.ts') != ''
shell: bash
run: npm run test:lp6:docker

- name: Validate LP3 mail images on amd64
if: inputs.architecture_mode != 'multiarch' && hashFiles('docker/lp3-tls/Dockerfile') != '' && hashFiles('docker/lp3-postfix/Dockerfile') != '' && hashFiles('docker/lp3-dovecot/Dockerfile') != '' && hashFiles('docker/lp3-rspamd/Dockerfile') != '' && hashFiles('docker/lp3-clamav/Dockerfile') != '' && hashFiles('docker/lp3-proof/Dockerfile') != ''
shell: bash
Expand Down
32 changes: 32 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,15 @@ passed the integrated GitHub AMD64 Compose proof, followed by the final ARM64
artifact and attestation gate. This remains local-proof evidence, not a claim
of production capacity or external service interoperability.

LP6 is implemented at the bounded local backup, restore, retention, and
disaster-recovery boundary. Its synthetic proof is now being validated through
the AMD64-first GitHub gate, followed by the explicit ARM64 artifact gate;
the smoke harness checks the internal dual-stack network IPAM before starting
the one-shot workers, and the source fixture declares its container-internal
source path explicitly. Until both gates are green, LP6 is not marked as a
completed release milestone. This is still local synthetic evidence, not a
production backup, storage, or RPO/RTO claim.

### Security

- [x] no open relay;
Expand Down Expand Up @@ -203,6 +212,7 @@ gulogulo/
│ ├── lp3-local-mail.md
│ ├── lp4-local-web.md
│ ├── lp5-local-operations-capacity.md
│ ├── lp6-local-backup-dr.md
│ ├── mail-core.md
│ ├── rbac-admin-mfa.md
│ ├── release-readiness.md
Expand Down Expand Up @@ -232,6 +242,11 @@ gulogulo/
│ ├── lp5-compose-audit.ts
│ ├── lp5-compose-smoke.ts
│ ├── lp5-proof-check.ts
│ ├── lp6-source-fixture.ts
│ ├── lp6-backup-worker.ts
│ ├── lp6-restore-worker.ts
│ ├── lp6-compose-audit.ts
│ ├── lp6-compose-smoke.ts
│ ├── m10-release-audit.mjs
│ ├── container-patch.sh
│ └── runtime, fixture, and patch utilities
Expand All @@ -242,15 +257,31 @@ gulogulo/
│ ├── lp3-local-mail.json
│ ├── lp4-local-web.json
│ ├── lp5-local-operations-capacity.json
│ ├── lp6-local-backup-dr.json
│ └── v1-release-evidence.template.json
├── src/
│ ├── admin/ (TypeScript RBAC, delegation, quota, and admin tools)
│ ├── auth/ (TypeScript password, TOTP, WebAuthn, and recovery contracts)
│ ├── backup/
│ │ ├── backup-contract.mjs
│ │ ├── backup-contract.test.mjs
│ │ ├── backup-contract.ts
│ │ ├── backup-contract.test.ts
│ │ ├── index.mjs
│ │ └── index.ts
│ ├── db/migrations/
│ ├── foundation/
│ ├── integrations/ (TypeScript LDAP, PostgreSQL, tenant, and migration adapters)
│ ├── lifecycle/
│ │ ├── account-lifecycle.mjs
│ │ ├── account-lifecycle.test.mjs
│ │ ├── account-lifecycle.ts
│ │ ├── account-lifecycle.test.ts
│ │ ├── index.mjs
│ │ ├── retention.mjs
│ │ ├── retention.test.mjs
│ │ ├── retention.ts
│ │ └── retention.test.ts
│ ├── mail/
│ │ ├── imap-idle.mjs
│ │ ├── imap-idle.test.ts
Expand Down Expand Up @@ -316,6 +347,7 @@ gulogulo/
├── tsconfig.json
├── tsconfig.lp4.json
├── tsconfig.lp5.json
├── tsconfig.lp6.json
└── tsconfig.server.json
~~~

Expand Down
119 changes: 119 additions & 0 deletions compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -989,6 +989,108 @@ services:
com.sythos.gulogulo.network-policy: offline_dependencies
com.sythos.gulogulo.protocols: patch_status_only

# LP6 is an offline, synthetic backup/restore/retention rehearsal. The
# source data volume is intentionally absent from the isolated restore
# service; restore can consume only a read-only backup volume.
gulogulo-lp6-source-fixture:
build:
context: .
dockerfile: Dockerfile
args:
INSTALL_DEV: "true"
platform: ${GULOGULO_LP6_COMPOSE_PLATFORM:-linux/amd64}
profiles: ["lp6-check"]
command: ["node", "--experimental-strip-types", "scripts/lp6-source-fixture.ts"]
# Named Docker volumes are initially root-owned. These disposable local
# writers need no capabilities and have no host or socket access.
user: "0:0"
environment:
LP6_SOURCE_DIR: /var/lib/gulogulo/lp6-source
volumes:
- lp6-source-data:/var/lib/gulogulo/lp6-source
networks:
- lp6-runtime
read_only: true
tmpfs:
- /tmp:rw,noexec,nosuid,size=16m
security_opt:
- no-new-privileges:true
cap_drop:
- ALL
init: true
restart: "no"
labels:
com.sythos.gulogulo.milestone: LP6
com.sythos.gulogulo.proof: local_synthetic_backup_source
com.sythos.gulogulo.network-policy: offline_dependencies

gulogulo-lp6-backup:
build:
context: .
dockerfile: Dockerfile
args:
INSTALL_DEV: "true"
platform: ${GULOGULO_LP6_COMPOSE_PLATFORM:-linux/amd64}
profiles: ["lp6"]
command: ["node", "--experimental-strip-types", "scripts/lp6-backup-worker.ts"]
user: "0:0"
environment:
LP6_SOURCE_DIR: /var/lib/gulogulo/lp6-source
LP6_BACKUP_DIR: /var/lib/gulogulo/lp6-backup
LP6_TEST_KEY_B64: ${LP6_TEST_KEY_B64}
LP6_KEY_REFERENCE: ${LP6_KEY_REFERENCE:-kms/local-synthetic/lp6-v1}
volumes:
- lp6-source-data:/var/lib/gulogulo/lp6-source:ro
- lp6-backup-data:/var/lib/gulogulo/lp6-backup
networks:
- lp6-runtime
read_only: true
tmpfs:
- /tmp:rw,noexec,nosuid,size=16m
security_opt:
- no-new-privileges:true
cap_drop:
- ALL
init: true
restart: "no"
labels:
com.sythos.gulogulo.milestone: LP6
com.sythos.gulogulo.proof: local_synthetic_encrypted_backup
com.sythos.gulogulo.network-policy: offline_dependencies

gulogulo-lp6-restore:
build:
context: .
dockerfile: Dockerfile
args:
INSTALL_DEV: "true"
platform: ${GULOGULO_LP6_COMPOSE_PLATFORM:-linux/amd64}
profiles: ["lp6-check"]
command: ["node", "--experimental-strip-types", "scripts/lp6-restore-worker.ts"]
user: "0:0"
environment:
LP6_BACKUP_DIR: /var/lib/gulogulo/lp6-backup
LP6_RESTORE_DIR: /var/lib/gulogulo/lp6-restore
LP6_TEST_KEY_B64: ${LP6_TEST_KEY_B64}
volumes:
- lp6-backup-data:/var/lib/gulogulo/lp6-backup:ro
- lp6-restore-data:/var/lib/gulogulo/lp6-restore
networks:
- lp6-runtime
read_only: true
tmpfs:
- /tmp:rw,noexec,nosuid,size=16m
security_opt:
- no-new-privileges:true
cap_drop:
- ALL
init: true
restart: "no"
labels:
com.sythos.gulogulo.milestone: LP6
com.sythos.gulogulo.proof: local_synthetic_isolated_restore
com.sythos.gulogulo.network-policy: offline_dependencies

volumes:
runtime-state:
name: ${GULOGULO_VOLUME_PREFIX:-gulogulo}-runtime-state
Expand Down Expand Up @@ -1071,6 +1173,15 @@ volumes:
lp5-patch-state:
name: ${GULOGULO_VOLUME_PREFIX:-gulogulo}-lp5-patch-state
external: ${GULOGULO_LP5_VOLUMES_EXTERNAL:-false}
lp6-source-data:
name: ${GULOGULO_VOLUME_PREFIX:-gulogulo}-lp6-source-data
external: ${GULOGULO_LP6_VOLUMES_EXTERNAL:-false}
lp6-backup-data:
name: ${GULOGULO_VOLUME_PREFIX:-gulogulo}-lp6-backup-data
external: ${GULOGULO_LP6_VOLUMES_EXTERNAL:-false}
lp6-restore-data:
name: ${GULOGULO_VOLUME_PREFIX:-gulogulo}-lp6-restore-data
external: ${GULOGULO_LP6_VOLUMES_EXTERNAL:-false}

networks:
proof-runtime:
Expand Down Expand Up @@ -1111,3 +1222,11 @@ networks:
config:
- subnet: 172.29.5.0/24
- subnet: fd42:4755:756c:7035::/64
lp6-runtime:
name: ${GULOGULO_LP6_NETWORK:-gulogulo-lp6-runtime}
internal: true
enable_ipv6: true
ipam:
config:
- subnet: 172.29.6.0/24
- subnet: fd42:4755:756c:7036::/64
Loading