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
7 changes: 7 additions & 0 deletions .github/workflows/quality-gates.yml
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ jobs:
test -f src/observability/alert-policy.mjs
test -f src/observability/observability.test.mjs
test -f doc/lifecycle-backup-dr.md
test -f doc/local-proof-scope.md
test -f src/ops/acme/index.mjs
test -f src/ops/acme/index.test.mjs
test -f src/ops/abuse/index.mjs
Expand All @@ -150,6 +151,10 @@ jobs:
test -f src/release/index.mjs
test -f src/release/release-evidence.mjs
test -f src/release/release-evidence.test.mjs
test -f src/release/local-proof-scope.mjs
test -f src/release/local-proof-scope.test.mjs
test -f release/local-proof-scope.json
test -f scripts/lp0-scope-audit.mjs
test -f doc/upgrade-and-migration.md
grep -Fq 'profiles: ["local"]' compose.yaml
grep -Fq 'profiles: ["test"]' compose.yaml
Expand All @@ -169,6 +174,7 @@ jobs:
scripts/m0-smoke.ps1 \
scripts/m1-fixture-smoke.ps1 \
scripts/container-patch.sh \
scripts/lp0-scope-audit.mjs \
scripts/m10-release-audit.mjs \
.gitignore \
.github/workflows/commit-tests.yml \
Expand Down Expand Up @@ -280,6 +286,7 @@ jobs:
node -e "const s=require('./config/schema.v1.json'); if (!s.properties.buildDigest || !s.properties.patching) process.exit(1)"
test -f package-lock.json
node -e "const e=require('./release/v1-release-evidence.template.json'); if (e.spdxLicenseIdentifier !== 'MIT' || e.author !== 'Sythos (https://www.sythos.net)' || e.product !== 'Gulo Gulo') process.exit(1)"
node -e "const s=require('./release/local-proof-scope.json'); if (s.spdxLicenseIdentifier !== 'MIT' || s.author !== 'Sythos (https://www.sythos.net)' || s.proofType !== 'local' || s.networkPolicy !== 'offline_runtime' || s.syntheticDataOnly !== true || s.publicDnsRequired !== false || s.publicAcmeEnabled !== false || s.externalPhaseDeferred !== true) process.exit(1)"

- name: Set up Node.js when package metadata exists
if: hashFiles('package.json') != ''
Expand Down
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ gulogulo/
│ ├── dav-and-discovery.md
│ ├── identity-and-postgres.md
│ ├── lifecycle-backup-dr.md
│ ├── local-proof-scope.md
│ ├── mail-core.md
│ ├── rbac-admin-mfa.md
│ ├── release-readiness.md
Expand All @@ -135,10 +136,12 @@ gulogulo/
├── scripts/
│ ├── m0-smoke.ps1
│ ├── m1-fixture-smoke.ps1
│ ├── lp0-scope-audit.mjs
│ ├── m10-release-audit.mjs
│ ├── container-patch.sh
│ └── runtime, fixture, and patch utilities
├── release/
│ ├── local-proof-scope.json
│ └── v1-release-evidence.template.json
├── src/
│ ├── admin/
Expand All @@ -152,6 +155,8 @@ gulogulo/
│ ├── observability/
│ ├── release/
│ │ ├── index.mjs
│ │ ├── local-proof-scope.mjs
│ │ ├── local-proof-scope.test.mjs
│ │ ├── release-evidence.mjs
│ │ └── release-evidence.test.mjs
│ ├── ops/
Expand Down
3 changes: 3 additions & 0 deletions doc/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,9 @@ normal workflow.
- [Release readiness](release-readiness.md) — the Section 30 evidence object,
hardening review matrix, sanitized provider API/MCP read surface, and the
honest boundary between a usable contract preview and a production service.
- [LP0 local proof scope](local-proof-scope.md) — the reserved local names,
synthetic-data rule, offline runtime boundary, disposable service inventory,
architecture targets, and the explicit deferral of the external phase.

The project is still intentionally small. The documents describe real behavior
only: DAV, administration, lifecycle, backup, observability, ACME, abuse, and
Expand Down
126 changes: 126 additions & 0 deletions doc/local-proof-scope.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
# LP0 local proof scope

<!--
SPDX-License-Identifier: MIT
SPDX-FileCopyrightText: 2026 Sythos (https://www.sythos.net)
Author: Sythos (https://www.sythos.net)
-->

LP0 is the deliberately boring freeze before the first local deployment
rehearsal. It says exactly what the local proof is allowed to prove, what it
must never accidentally touch, and which services LP1 and the later local
milestones will add. It is a contract, not a claim that the current scaffold is
already a complete mail provider.

## The short version

The local proof is an isolated, synthetic-data-only deployment of Gulo Gulo.
It uses reserved names, an internal certificate authority, and an offline
runtime network. It does not need a real domain, public DNS, public ACME, real
mailboxes, or a connection to an external mail system. The local release label
is `v0.1.0-local-proof.1`.

The machine-readable source for this boundary is
[`release/local-proof-scope.json`](../release/local-proof-scope.json). The
validator and its tests live in `src/release/local-proof-scope.mjs` and
`src/release/local-proof-scope.test.mjs`; `npm run test:lp0` and
`npm run test:lp0:audit` are the two convenient entry points.

## What LP0 freezes

### Names and certificates

- `gulogulo.test` is the synthetic tenant/domain name.
- `webmail.localhost`, `calendar.localhost`, and `contacts.localhost` are
local browser endpoints.
- `.test` and `.localhost` are reserved for this rehearsal. They must not be
replaced with a real public domain during LP1–LP9.
- TLS is terminated with a locally trusted CA generated for the proof. Public
Let's Encrypt and generic public ACME are explicitly disabled here.
- Host-file or local DNS entries may point these names at loopback or the
private Compose/Kubernetes test network. No public DNS record is created.

### Data and identities

- Every account, message, attachment, calendar object, contact, quota, and
audit event is synthetic.
- No production credentials, private keys, mailbox exports, real recipient
addresses, or user-generated personal data may enter the proof.
- LDAP and PostgreSQL are local disposable dependencies in LP1 and later. They
are not the provider's external identity or application database.
- The local tenant remains isolated from every other test run by project name,
network, and external-volume namespace.

### Runtime network boundary

The build may download pinned or latest-stable build inputs when the operator
explicitly runs a build. Once the proof is running, application and dependency
containers use an `offline_runtime` policy: no Internet egress, no public DNS,
no public ACME challenges, and no access to a Docker socket. A local network is
allowed only for the declared Gulo Gulo services and their health checks.

The policy is intentionally stronger than “we promise not to send mail”. It
prevents an accidental update check, telemetry call, public certificate
request, or real SMTP delivery from turning a local test into an external
operation.

## Service inventory for LP1–LP9

LP0 freezes the inventory; it does not claim that each service is already
implemented. The local proof will add and exercise these components in later
milestones:

| Service | Local proof role |
|---|---|
| `gulogulo` | WebWare/API, session, tenant and read-only monitoring surface |
| `ldap` | Disposable local identity directory |
| `postgresql` | Disposable local application state and quota ledger |
| `postfix` | Local SMTP ingress/egress simulation with no public delivery |
| `dovecot` | Local IMAP and IMAP IDLE mailbox behavior |
| `rspamd` | Local message-scanning verdicts and queue metadata |
| `clamav` | Local antivirus scanning contract and safe failure mode |
| `caldav` | Local calendar protocol endpoint |
| `carddav` | Local address-book protocol endpoint |
| `local-dns` | Reserved-name resolution inside the test boundary |
| `local-ca` | Disposable trust root and endpoint certificates |

The implementation can package more than one logical role in a container when
that is useful for the first rehearsal, but the role boundaries and health
contracts remain visible. The final provider deployment may split them again.

## Architecture targets

Images and Compose/Kubernetes manifests target both `linux/amd64` (x86_64) and
`linux/arm64`. A developer may run only the host architecture locally; CI still
validates both image targets. The LP0 manifest is the source of truth for this
platform pair and the local-only release identity.

## Acceptance criteria

LP0 is complete when all of these remain true in a clean checkout:

1. `release/local-proof-scope.json` validates without network access.
2. The manifest declares only reserved local names and synthetic data.
3. Public DNS and public ACME are false, and the runtime policy is
`offline_runtime`.
4. The required service inventory and both architecture targets are complete.
5. The LP0 test and audit commands pass locally and in the pull-request and
post-merge GitHub Actions gates.
6. No container is started and no external system is contacted as part of LP0.

## Explicitly deferred

The following belong to a later external/public phase and must not be smuggled
into the local proof:

- registration or use of a real domain and public DNS;
- public ACME/Let's Encrypt challenges and public certificate renewal;
- delivery to or acceptance from real Internet mail systems;
- production LDAP, PostgreSQL, storage, backup, or monitoring endpoints;
- public registry publication, signed production image release, and live
Kubernetes traffic switching;
- production RPO/RTO evidence, incident rehearsal, and standard-client testing
against real provider infrastructure.

When that phase is eventually approved, it gets a separate release identity and
separate evidence. It does not overwrite the local proof manifest.
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,14 @@
"build:web": "node web/build.mjs",
"typecheck": "tsc --project tsconfig.json --noEmit",
"test:web": "npm run build:web && node web/test/web-shell.test.mjs",
"test": "npm run test:web && npm run test:m6 && npm run test:m7 && npm run test:m8 && npm run test:m9 && npm run test:m10 && node src/runtime/runtime.test.mjs && node src/foundation/config.test.mjs && node src/runtime/observability.test.mjs && node src/integrations/tenant-context.test.mjs && node src/integrations/ldap-client.test.mjs && node src/integrations/postgres-store.test.mjs && node src/mail/mail-core.test.mjs && node src/web/security/security.test.mjs && node src/web/content/email-content.test.mjs && node src/web/content/attachment-policy.test.mjs && node src/web/content/timezone.test.mjs && node src/web/realtime/event-normalizer.test.mjs && node src/web/backup/backup-request.test.mjs && node src/dav/caldav/caldav-contract.test.mjs && node src/dav/carddav/carddav-store.test.mjs && node src/dav/discovery/index.test.mjs",
"test": "npm run test:web && npm run test:m6 && npm run test:m7 && npm run test:m8 && npm run test:m9 && npm run test:m10 && npm run test:lp0 && node src/runtime/runtime.test.mjs && node src/foundation/config.test.mjs && node src/runtime/observability.test.mjs && node src/integrations/tenant-context.test.mjs && node src/integrations/ldap-client.test.mjs && node src/integrations/postgres-store.test.mjs && node src/mail/mail-core.test.mjs && node src/web/security/security.test.mjs && node src/web/content/email-content.test.mjs && node src/web/content/attachment-policy.test.mjs && node src/web/content/timezone.test.mjs && node src/web/realtime/event-normalizer.test.mjs && node src/web/backup/backup-request.test.mjs && node src/dav/caldav/caldav-contract.test.mjs && node src/dav/carddav/carddav-store.test.mjs && node src/dav/discovery/index.test.mjs",
"test:m6": "node src/admin/rbac.test.mjs && node src/admin/delegation.test.mjs && node src/admin/quota.test.mjs && node src/admin/admin-tools.test.mjs && node src/auth/auth.test.mjs",
"test:m7": "node src/lifecycle/retention.test.mjs && node src/lifecycle/account-lifecycle.test.mjs && node src/backup/backup-contract.test.mjs && node src/observability/observability.test.mjs",
"test:m8": "node src/ops/acme/index.test.mjs && node src/ops/abuse/index.test.mjs",
"test:m9": "node src/upgrade/upgrade-contract.test.mjs",
"test:m10": "node src/release/release-evidence.test.mjs && node scripts/m10-release-audit.mjs",
"test:lp0": "node src/release/local-proof-scope.test.mjs && node scripts/lp0-scope-audit.mjs",
"test:lp0:audit": "node scripts/lp0-scope-audit.mjs",
"test:m2:postgres": "node src/integrations/postgres.integration.test.mjs"
},
"dependencies": {
Expand Down
38 changes: 38 additions & 0 deletions release/local-proof-scope.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
{
"$comment": "SPDX-License-Identifier: MIT; SPDX-FileCopyrightText: 2026 Sythos (https://www.sythos.net); Author: Sythos (https://www.sythos.net)",
"spdxLicenseIdentifier": "MIT",
"spdxFileCopyrightText": "2026 Sythos (https://www.sythos.net)",
"author": "Sythos (https://www.sythos.net)",
"schemaVersion": 1,
"proofType": "local",
"releaseLabel": "v0.1.0-local-proof.1",
"networkPolicy": "offline_runtime",
"syntheticDataOnly": true,
"publicDnsRequired": false,
"publicAcmeEnabled": false,
"targetPlatforms": [
"linux/amd64",
"linux/arm64"
],
"localNames": [
"gulogulo.test",
"webmail.localhost",
"calendar.localhost",
"contacts.localhost"
],
"requiredServices": [
"gulogulo",
"ldap",
"postgresql",
"postfix",
"dovecot",
"rspamd",
"clamav",
"caldav",
"carddav",
"local-dns",
"local-ca"
],
"externalPhaseDeferred": true,
"status": "frozen"
}
26 changes: 26 additions & 0 deletions scripts/lp0-scope-audit.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// SPDX-License-Identifier: MIT
// SPDX-FileCopyrightText: 2026 Sythos (https://www.sythos.net)
// Author: Sythos (https://www.sythos.net)

import { readFile } from 'node:fs/promises';
import { resolve } from 'node:path';

import { createLocalProofScope } from '../src/release/local-proof-scope.mjs';

const manifestPath = resolve(process.cwd(), 'release/local-proof-scope.json');
const manifest = JSON.parse(await readFile(manifestPath, 'utf8'));
const scope = createLocalProofScope(manifest);

console.log(JSON.stringify({
proofType: scope.proofType,
releaseLabel: scope.releaseLabel,
networkPolicy: scope.networkPolicy,
syntheticDataOnly: scope.syntheticDataOnly,
publicDnsRequired: scope.publicDnsRequired,
publicAcmeEnabled: scope.publicAcmeEnabled,
targetPlatforms: scope.targetPlatforms,
localNames: scope.localNames,
requiredServices: scope.requiredServices,
externalPhaseDeferred: scope.externalPhaseDeferred,
status: scope.status,
}, null, 2));
7 changes: 7 additions & 0 deletions src/release/index.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,10 @@ export {
createReleaseEvidence,
evaluateReleaseEvidence,
} from './release-evidence.mjs';

export {
LOCAL_PROOF_RELEASE_LABEL,
LOCAL_PROOF_REQUIRED_SERVICES,
PLATFORM_SET,
createLocalProofScope,
} from './local-proof-scope.mjs';
Loading