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
11 changes: 11 additions & 0 deletions .github/workflows/quality-gates.yml
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,9 @@ jobs:
test -f doc/upgrade-and-migration.md
test -f doc/web-foundation.md
test -f doc/dav-and-discovery.md
test -f doc/release-readiness.md
test -f release/v1-release-evidence.template.json
test -f scripts/m10-release-audit.mjs
test -f tsconfig.json
test -f web/index.html
test -f web/styles.css
Expand Down Expand Up @@ -144,6 +147,9 @@ jobs:
test -f src/upgrade/rollout.mjs
test -f src/upgrade/index.mjs
test -f src/upgrade/upgrade-contract.test.mjs
test -f src/release/index.mjs
test -f src/release/release-evidence.mjs
test -f src/release/release-evidence.test.mjs
test -f doc/upgrade-and-migration.md
grep -Fq 'profiles: ["local"]' compose.yaml
grep -Fq 'profiles: ["test"]' compose.yaml
Expand All @@ -163,6 +169,7 @@ jobs:
scripts/m0-smoke.ps1 \
scripts/m1-fixture-smoke.ps1 \
scripts/container-patch.sh \
scripts/m10-release-audit.mjs \
.gitignore \
.github/workflows/commit-tests.yml \
.github/workflows/pr-validation.yml \
Expand Down Expand Up @@ -240,6 +247,9 @@ jobs:
grep -Fq 'createDockerReplacementPlan' src/upgrade/rollout.mjs
grep -Fq 'createKubernetesBlueGreenPlan' src/upgrade/rollout.mjs
grep -Fq 'ACTION_NOT_ALLOWLISTED' src/upgrade/rollout.mjs
grep -Fq 'REQUIRED_SECTION30_ITEMS' src/release/release-evidence.mjs
grep -Fq 'M10_REVIEWS_INCOMPLETE' src/release/release-evidence.mjs
grep -Fq 'productionReady' src/release/release-evidence.mjs

grep -Fq 'SPDX-License-Identifier: MIT' config/schema.v1.json
grep -Fq 'Sythos (https://www.sythos.net)' config/schema.v1.json
Expand Down Expand Up @@ -269,6 +279,7 @@ jobs:
node -e "const p=require('./package-lock.json'); const root=p.packages && p.packages['']; if (!root || root.license !== 'MIT' || root.author !== 'Sythos (https://www.sythos.net)' || root.spdxLicenseIdentifier !== 'MIT') process.exit(1)"
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)"

- name: Set up Node.js when package metadata exists
if: hashFiles('package.json') != ''
Expand Down
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,12 +127,19 @@ gulogulo/
│ ├── lifecycle-backup-dr.md
│ ├── mail-core.md
│ ├── rbac-admin-mfa.md
│ ├── release-readiness.md
│ ├── observability.md
│ ├── storage-and-quotas.md
│ ├── upgrade-and-migration.md
│ └── web-foundation.md
├── scripts/
│ ├── m0-smoke.ps1
│ ├── m1-fixture-smoke.ps1
│ ├── m10-release-audit.mjs
│ ├── container-patch.sh
│ └── runtime, fixture, and patch utilities
├── release/
│ └── v1-release-evidence.template.json
├── src/
│ ├── admin/
│ ├── auth/
Expand All @@ -143,6 +150,10 @@ gulogulo/
│ ├── lifecycle/
│ ├── mail/
│ ├── observability/
│ ├── release/
│ │ ├── index.mjs
│ │ ├── release-evidence.mjs
│ │ └── release-evidence.test.mjs
│ ├── ops/
│ │ ├── abuse/
│ │ └── acme/
Expand Down
17 changes: 10 additions & 7 deletions doc/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,12 +51,15 @@ normal workflow.
calendar and address-book contracts, conditional writes, ETags, sync tokens,
iCalendar/vCard validation, `.well-known` responses, and safe manual
configuration fallback.
- [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.

The project is still intentionally small. The documents describe real behavior
only: DAV, administration, lifecycle, backup, observability, ACME, and abuse
are deterministic contract boundaries until their external adapters and
rehearsals are in place. The M9 upgrade module adds the validation and state
machine, but production deployment still needs a persistent DAV backend,
authenticated HTTP method adapter, real ACME/DNS operations, measured RPO/RTO,
an actual Docker/Kubernetes cutover, and standard-client interoperability
rehearsal before it is treated as a complete external service.
only: DAV, administration, lifecycle, backup, observability, ACME, abuse, and
upgrade are deterministic contract boundaries until their external adapters and
rehearsals are in place. The release-readiness guide collects those limits in a
machine-checkable decision, but production deployment still needs a persistent
DAV backend, authenticated HTTP method adapter, real ACME/DNS operations,
measured RPO/RTO, an actual Docker/Kubernetes cutover, and standard-client
interoperability rehearsal before it is treated as a complete external service.
185 changes: 185 additions & 0 deletions doc/release-readiness.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
# Release readiness and the M10 boundary

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

This is the slightly boring document that keeps a release honest. M10 does not
turn a fixture into a mail provider by changing the wording around it. It
packages the review into a repeatable evidence object, runs the security and
tenant-boundary checks, and makes the remaining external work visible to the
operator.

The useful result at this point is a **V1 contract preview**: a clean checkout
can be installed, tested, inspected, and used as the application/runtime
foundation. A production mail service still needs the provider adapters and a
rehearsal against real Postfix, Dovecot, LDAP, PostgreSQL, CalDAV, CardDAV,
certificate, scanner, backup, and traffic-switching services. Those are
explicit residuals, not hidden assumptions.

## What M10 checks

The M10 gate covers five evidence domains:

| Domain | What is checked in this checkout | What still needs an external rehearsal |
|---|---|---|
| Security | tenant/RBAC boundaries, session and CSRF contracts, MFA primitives, HTML sanitization, abuse limits, and secret-free audit events | provider secret rotation, real TLS/LDAP/DB configuration, image signing and SBOM publication |
| Data | quota allocation, 28-day purge, backup authorization, encrypted archive shape, idempotent lifecycle operations | an actual restore, deletion runbook execution, external-volume snapshots, measured RPO/RTO |
| Interoperability | SMTP/IMAP/IDLE, Sieve, DAV object semantics, discovery, ICS/vCard, and timezone contracts | vendor client matrix and real protocol endpoints |
| Operations | health, metrics, logging, alerts, queue visibility, Docker/Kubernetes migration contracts, and multi-architecture build configuration | scanner update channels, Docker host replacement, Kubernetes cutover, rollback, and incident tabletop |
| Governance | role and delegation policy, default-deny master access, read-only API/MCP, ADRs, and documentation inventory | owner approval of the deployment and disaster-recovery runbooks |

The source of truth for the checklist remains Section 30 of `GULOGULO.md`.
The repository copy is deliberately an evidence boundary, not a second product
specification.

## Running the gate

From a clean checkout, install the locked dependencies and run the complete
suite:

```text
npm ci --ignore-scripts --no-audit --no-fund
npm test
```

The focused M10 checks can be run while iterating:

```text
npm run test:m10
```

That command runs `src/release/release-evidence.test.mjs` and then audits the
portable example at `release/v1-release-evidence.template.json`. The audit
prints only a sanitized decision summary. It never prints credentials, raw
deployment output, mailbox content, or local workstation paths.

## Evidence object

The validator lives in `src/release/release-evidence.mjs`. Its public API is
small on purpose:

```js
import {
createReleaseEvidence,
evaluateReleaseEvidence,
REQUIRED_SECTION30_ITEMS,
} from './src/release/release-evidence.mjs';

const evidence = createReleaseEvidence({
evidenceVersion: '1.0',
product: 'Gulo Gulo',
version: '0.0.0-m10',
commitSha: '0123456789abcdef0123456789abcdef01234567',
generatedAt: '2026-08-23T00:00:00Z',
releaseDecision: 'conditional',
section30: [/* every REQUIRED_SECTION30_ITEMS entry */],
securityFindings: [],
tests: [],
artifacts: [{
name: 'operator guide',
path: 'doc/release-readiness.md',
purpose: 'Release boundary and evidence instructions.',
}],
residualRisks: ['External service rehearsal is still required.'],
nextCandidates: ['Wire provider adapters.'],
});

const summary = evaluateReleaseEvidence(evidence);
```

`createReleaseEvidence` returns a frozen, normalized object. It enforces the
following rules before a release can be discussed:

- all 45 applicable Section 30 item IDs must occur exactly once;
- `verified` and `contract` entries must point to repository-relative evidence;
- `deferred` and `exception` entries must name an owner, mitigation, rationale,
and dated approval;
- critical or high findings cannot remain `open`;
- failed tests block the release;
- artifacts, rationale, commands, and residual risks are scanned for secrets,
private keys, tokens, and workstation paths;
- an `approved` decision is rejected when the checklist still contains a
deferred or exception item.

There are four checklist statuses:

- `verified` means the local and CI evidence is sufficient for the stated
contract;
- `contract` means the behavior is specified and tested at the boundary, but a
vendor or deployment environment must still prove it;
- `deferred` means the item is intentionally left for the provider rehearsal;
- `exception` is the same kind of explicit deferral when the owner wants to
call out a formal non-conformance.

`evaluateReleaseEvidence` reports `productionReady: false` whenever a contract,
deferred, exception, conditional test, or accepted security finding remains.
That conservative result is intentional: a green repository check is not a
certificate, a live LDAP bind, or a measured zero-downtime cutover.

## API and MCP handling

The tenant monitoring API and MCP stay read-only in V1. M10 adds a release
evidence vocabulary; it does not add a write-capable release endpoint.

The provider/operator plane may expose a sanitized read operation equivalent to
the following contract:

| Surface | Operation | Result |
|---|---|---|
| HTTP | `GET /provider/release/evidence` | current decision, checklist counts, test statuses, residual risks, and next candidates |
| MCP | `gulogulo.release.evidence` | the same read-only object, scoped to the provider deployment |
| HTTP | `GET /provider/release/capabilities` | evidence schema version and supported status values |
| MCP | `gulogulo.release.capabilities` | the same capability document |

The response must be derived from `createReleaseEvidence` before publication.
It must contain the release version, commit identifier, Section 30 status
counts, sanitized evidence references, and correlation metadata. It must not
contain passwords, tokens, private keys, mailbox content, raw command output,
Docker socket paths, unrestricted Kubernetes arguments, or absolute local
paths.

The tenant, master, and user audiences may read only the subset allowed by the
existing RBAC and log-visibility policy. They cannot approve an exception,
change a status, start an upgrade, or mark a live rehearsal complete. A
provider approval remains an auditable, separate operation.

## Login and browser boundary

The current HTML5/TypeScript shell is deliberately honest about its stage. It
uses secure-cookie and CSRF contracts, renders mail/calendar/contact views,
and refuses to treat realtime metadata or message HTML as trusted application
content. The complete authenticated `/login` route and its provider LDAP
adapter are not faked by the release audit. When that route is wired, it must
keep the Gulo Gulo artwork at the left of the login layout, scale it to
128×128, and keep the form on the right as recorded in the canonical artwork
memory.

## External-evidence checklist

Before calling a deployment production-ready, an operator should attach these
records to the release commit or release system:

1. Docker `linux/amd64` and `linux/arm64` image digests, SBOM, and signature.
2. TLS/ACME issuance, renewal, expiry alert, LDAP bind, and PostgreSQL backup
evidence.
3. Postfix, Rspamd, ClamAV, Dovecot, Sieve, CalDAV, CardDAV, and autodiscovery
client results.
4. Encrypted backup, tenant/user restore, purge, and account deletion records.
5. Docker replacement and Kubernetes blue/green timings, including rollback
and connection-drain evidence.
6. Approved RPO/RTO, incident, and disaster-recovery runbooks.

When these records exist, replace the corresponding `deferred` or `contract`
entries in a release evidence object with `verified`, use the actual release
commit SHA, and rerun `npm run test:m10` plus the GitHub Actions quality gates.

## Future work deliberately outside M10

Shared mailboxes, resource calendars, write-capable tenant API/MCP, assisted
IMAP migration, and provider-specific live adapters remain next-version or
deployment work. Keeping them out of this gate makes the product smaller and
safer to review; it does not prevent adding them later behind a new contract
and a new acceptance record.
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,12 @@
"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 && 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 && 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:m2:postgres": "node src/integrations/postgres.integration.test.mjs"
},
"dependencies": {
Expand Down
Loading