+ If Realm Chat is activated, admitted recipients receive the permitted live and
+ historical message views described above, and authorized moderators may receive the
+ exact reported message, report context, and related private records. Reporters do not
+ receive another player's private moderation outcome. Warpkeep does not sell chat or
+ report data and does not use it for advertising.
+
These are global online services, so information may be processed in multiple
countries, including outside the country where you live or access the Alpha. The
@@ -338,6 +363,12 @@
+ Realm Chat's age and minor-participation policy is unresolved. The project must not
+ activate it until the owner and qualified legal reviewer approve an age policy and
+ any required parental, guardian, notice, consent, or access measures. No age threshold
+ is asserted by this draft.
+
The current Alpha has no complete core strategy loop, alliance system, combat system,
- or in-game chat. Lore and design drafts are direction, not hidden mechanics, paid
- power, rewards, punishments, or future entitlements. A gameplay system becomes real
- only when its rules and server authority are implemented and published.
+ or active in-game chat. Realm Chat is a disabled proposal pending owner and legal
+ review. If later activated, one Realm channel will let admitted Keepers speak in the
+ shared world. Messages may persist as Realm history; local muting affects only the
+ muting player's current session. Reporting must preserve the exact reported message
+ and context for private review. Reports do not automatically punish anyone, and
+ knowingly false, retaliatory, or abusive reports may themselves affect access.
+ Lore and design drafts are direction, not hidden mechanics, paid power, rewards,
+ punishments, or future entitlements. A gameplay system becomes real only when its
+ rules, privacy notice, server authority, and activation state are implemented and
+ published.
+
+
+
+
@@ -112,7 +125,8 @@ 5. Privacy and availability
The Privacy Notice describes authentication,
public game projections, private authority data, providers, retention limits, and
- privacy choices. Some public Realm state intentionally links an admitted FID and
+ privacy choices, including the additional records that a future Realm Chat would
+ create. Some public Realm state intentionally links an admitted FID and
trusted public Farcaster presentation to a castle. Private balances, daily-grant
receipts, and ownership authority remain private; an aggregate Mark balance may be
included in the player's enabled public community projection.
diff --git a/scripts/entry-agreement-policy.d.mts b/scripts/entry-agreement-policy.d.mts
index d47c5fbf..e7443ea6 100644
--- a/scripts/entry-agreement-policy.d.mts
+++ b/scripts/entry-agreement-policy.d.mts
@@ -1,16 +1,18 @@
export const WARPKEEP_ENTRY_AGREEMENT_VERSION:
- '2026-07-31-hegemony-entry-agreement-v4';
+ '2026-08-03-hegemony-entry-agreement-v5';
export const WARPKEEP_HISTORICAL_ENTRY_AGREEMENT_VERSIONS: readonly [
+ '2026-07-31-hegemony-entry-agreement-v4',
'2026-07-19-hegemony-entry-agreement-v3',
'2026-07-19-hegemony-entry-agreement-v2',
'2026-07-18-hegemony-entry-agreement-v1',
'2026-07-14',
];
export const WARPKEEP_ENTRY_AGREEMENT_EVIDENCE_VERSIONS: readonly [
+ '2026-08-03-hegemony-entry-agreement-v5',
'2026-07-31-hegemony-entry-agreement-v4',
'2026-07-19-hegemony-entry-agreement-v3',
'2026-07-19-hegemony-entry-agreement-v2',
'2026-07-18-hegemony-entry-agreement-v1',
'2026-07-14',
];
-export const WARPKEEP_ENTRY_AGREEMENT_ACCEPTANCE_RECORDS_PER_FID_MAXIMUM: 5;
+export const WARPKEEP_ENTRY_AGREEMENT_ACCEPTANCE_RECORDS_PER_FID_MAXIMUM: 6;
diff --git a/scripts/entry-agreement-policy.mjs b/scripts/entry-agreement-policy.mjs
index fb98351b..d833f6bb 100644
--- a/scripts/entry-agreement-policy.mjs
+++ b/scripts/entry-agreement-policy.mjs
@@ -4,9 +4,10 @@
* policy because Node cannot import the SpacetimeDB TypeScript module directly.
*/
export const WARPKEEP_ENTRY_AGREEMENT_VERSION =
- '2026-07-31-hegemony-entry-agreement-v4';
+ '2026-08-03-hegemony-entry-agreement-v5';
export const WARPKEEP_HISTORICAL_ENTRY_AGREEMENT_VERSIONS = Object.freeze([
+ '2026-07-31-hegemony-entry-agreement-v4',
'2026-07-19-hegemony-entry-agreement-v3',
'2026-07-19-hegemony-entry-agreement-v2',
'2026-07-18-hegemony-entry-agreement-v1',
diff --git a/scripts/qa-observer/local-fullstack-browser-probe.mjs b/scripts/qa-observer/local-fullstack-browser-probe.mjs
index 94aecdd2..d3868875 100644
--- a/scripts/qa-observer/local-fullstack-browser-probe.mjs
+++ b/scripts/qa-observer/local-fullstack-browser-probe.mjs
@@ -5209,7 +5209,7 @@ export async function runLocalFullstackBrowserProbe(options = {}) {
|| database.seedAttestation.workerCount !== 28
|| database.seedAttestation.entryAgreementAcceptedCurrent !== true
|| database.seedAttestation.entryAgreementRequiredVersion
- !== '2026-07-31-hegemony-entry-agreement-v4'
+ !== '2026-08-03-hegemony-entry-agreement-v5'
|| database.seedAttestation.genericAssignments !== 0
|| database.seedAttestation.genericOccupations !== 0
|| database.seedAttestation.genericSchedules !== 0
diff --git a/scripts/qa-observer/local-fullstack-spacetime.mjs b/scripts/qa-observer/local-fullstack-spacetime.mjs
index 8831bb65..5b12dfa5 100644
--- a/scripts/qa-observer/local-fullstack-spacetime.mjs
+++ b/scripts/qa-observer/local-fullstack-spacetime.mjs
@@ -39,7 +39,7 @@ const SERVER_STOP_TIMEOUT_MILLISECONDS = 5_000;
const PROFILE_POLICY_VERSION = 'trusted-snapchain-profile-v3';
const RESOURCE_POLICY_VERSION = 'genesis-resource-yield-v1';
const WORKER_PROTOCOL_CAPABILITY = 'generic-castle-workers-v1';
-const ENTRY_AGREEMENT_VERSION = '2026-07-31-hegemony-entry-agreement-v4';
+const ENTRY_AGREEMENT_VERSION = '2026-08-03-hegemony-entry-agreement-v5';
const LOCAL_FULLSTACK_FOUNDERS = Object.freeze(Array.from(
{ length: LOCAL_FULLSTACK_FOUNDER_COUNT },
(_, index) => Object.freeze({
diff --git a/scripts/verify-spacetime-additive-migration.mjs b/scripts/verify-spacetime-additive-migration.mjs
index 94c7eb54..24c6d794 100644
--- a/scripts/verify-spacetime-additive-migration.mjs
+++ b/scripts/verify-spacetime-additive-migration.mjs
@@ -104,12 +104,13 @@ const actualModuleOtherFid = 730_002;
const syntheticMissingAccessRequestFid = '9007199254740991';
const syntheticSecondAccessRequestFid = '9007199254740990';
const historicalEntryAgreementVersions = Object.freeze([
+ '2026-07-31-hegemony-entry-agreement-v4',
'2026-07-19-hegemony-entry-agreement-v3',
'2026-07-19-hegemony-entry-agreement-v2',
'2026-07-18-hegemony-entry-agreement-v1',
'2026-07-14',
]);
-const alphaTermsVersion = '2026-07-31-hegemony-entry-agreement-v4';
+const alphaTermsVersion = '2026-08-03-hegemony-entry-agreement-v5';
const resourcePolicyVersion = 'genesis-resource-yield-v1';
const marksPolicyVersion = 'admitted-daily-mark-v1';
const profilePolicyVersion = 'trusted-snapchain-profile-v3';
diff --git a/spacetimedb/src/entryAgreementPolicy.ts b/spacetimedb/src/entryAgreementPolicy.ts
index 66575af2..a2a38a25 100644
--- a/spacetimedb/src/entryAgreementPolicy.ts
+++ b/spacetimedb/src/entryAgreementPolicy.ts
@@ -4,7 +4,7 @@
* "terms" for deployed wire compatibility only.
*/
export const WARPKEEP_ENTRY_AGREEMENT_VERSION =
- '2026-07-31-hegemony-entry-agreement-v4';
+ '2026-08-03-hegemony-entry-agreement-v5';
/** Compatibility alias retained by existing reducer and client imports. */
export const WARPKEEP_ALPHA_TERMS_VERSION = WARPKEEP_ENTRY_AGREEMENT_VERSION;
@@ -15,6 +15,7 @@ export const WARPKEEP_ALPHA_TERMS_VERSION = WARPKEEP_ENTRY_AGREEMENT_VERSION;
* entry or gameplay requirement, which always compares the exact current ID.
*/
export const WARPKEEP_HISTORICAL_ENTRY_AGREEMENT_VERSIONS = Object.freeze([
+ '2026-07-31-hegemony-entry-agreement-v4',
'2026-07-19-hegemony-entry-agreement-v3',
'2026-07-19-hegemony-entry-agreement-v2',
'2026-07-18-hegemony-entry-agreement-v1',
diff --git a/spacetimedb/tests/entryAgreementPolicy.test.ts b/spacetimedb/tests/entryAgreementPolicy.test.ts
index 3cf4d217..7d953c3c 100644
--- a/spacetimedb/tests/entryAgreementPolicy.test.ts
+++ b/spacetimedb/tests/entryAgreementPolicy.test.ts
@@ -24,7 +24,7 @@ function source(path: string): string {
test('the current Hegemony entry agreement preserves the deployed Terms-shaped version alias', () => {
assert.equal(
WARPKEEP_ENTRY_AGREEMENT_VERSION,
- '2026-07-31-hegemony-entry-agreement-v4',
+ '2026-08-03-hegemony-entry-agreement-v5',
);
assert.equal(WARPKEEP_ALPHA_TERMS_VERSION, WARPKEEP_ENTRY_AGREEMENT_VERSION);
assert.equal(REEXPORTED_ALPHA_TERMS_VERSION, WARPKEEP_ENTRY_AGREEMENT_VERSION);
@@ -33,6 +33,7 @@ test('the current Hegemony entry agreement preserves the deployed Terms-shaped v
test('historical immutable evidence remains bounded and never becomes the current version', () => {
assert.deepEqual(WARPKEEP_HISTORICAL_ENTRY_AGREEMENT_VERSIONS, [
+ '2026-07-31-hegemony-entry-agreement-v4',
'2026-07-19-hegemony-entry-agreement-v3',
'2026-07-19-hegemony-entry-agreement-v2',
'2026-07-18-hegemony-entry-agreement-v1',
@@ -135,12 +136,12 @@ test('caller status reports missing exact-current evidence without mutation', ()
assert.ok(Object.isFrozen(result));
});
-test('a retained V3 acceptance is historical and cannot satisfy the V4 entry gate', () => {
+test('a retained V4 acceptance is historical and cannot satisfy the V5 entry gate', () => {
const fid = 101n;
- const v3Version = '2026-07-19-hegemony-entry-agreement-v3';
- const v3Key = `${fid}:${v3Version}`;
+ const v4Version = '2026-07-31-hegemony-entry-agreement-v4';
+ const v4Key = `${fid}:${v4Version}`;
const retainedEvidence = new Map([
- [v3Key, Object.freeze({ acceptanceKey: v3Key, fid, termsVersion: v3Version })],
+ [v4Key, Object.freeze({ acceptanceKey: v4Key, fid, termsVersion: v4Version })],
]);
const lookupKeys: string[] = [];
diff --git a/src/legal/alphaTermsPolicy.ts b/src/legal/alphaTermsPolicy.ts
index da0a1add..65afaa58 100644
--- a/src/legal/alphaTermsPolicy.ts
+++ b/src/legal/alphaTermsPolicy.ts
@@ -1,6 +1,6 @@
/** Exact version of the Social Contract incorporated by the current Terms. */
export const WARPKEEP_HEGEMONY_SOCIAL_CONTRACT_VERSION =
- '2026-07-19-HEGEMONY-SOCIAL-CONTRACT-V3';
+ '2026-08-03-HEGEMONY-SOCIAL-CONTRACT-V4';
/**
* Exact server-side identifier for the complete current entry-agreement bundle.
@@ -8,7 +8,7 @@ export const WARPKEEP_HEGEMONY_SOCIAL_CONTRACT_VERSION =
* acceptance records bind the complete bundle rather than only one document.
*/
export const WARPKEEP_ENTRY_AGREEMENT_VERSION =
- '2026-07-31-hegemony-entry-agreement-v4';
+ '2026-08-03-hegemony-entry-agreement-v5';
/**
* Retained deployed reducer/input name. It identifies the complete linked
@@ -22,8 +22,8 @@ export const WARPKEEP_ALPHA_TERMS_VERSION = WARPKEEP_ENTRY_AGREEMENT_VERSION;
* allowing the accepted document to drift behind an unchanged reducer value.
*/
export const WARPKEEP_ALPHA_TERMS_TEXT_SHA256 =
- 'b78bacc360df53f57bed668a68c311acf14e957156ecd8256e388a6ef38496bf';
+ 'dede7757c3be767b7a87e89e2c68817e9390cde91fabcf38246756afacdf51bd';
/** SHA-256 of the canonical Social Contract's normalized visible text. */
export const WARPKEEP_HEGEMONY_SOCIAL_CONTRACT_TEXT_SHA256 =
- 'a052a4f53aee749b702037f7a6eeb1e9dbd6fab0cbcd60aed81dacade8cbb66d';
+ '85941d066dd39f5be069d640f1419491e6fc0f691d01c292bfc3ed995c249110';
diff --git a/src/legal/realmChatPolicy.ts b/src/legal/realmChatPolicy.ts
new file mode 100644
index 00000000..2960fc08
--- /dev/null
+++ b/src/legal/realmChatPolicy.ts
@@ -0,0 +1,16 @@
+/**
+ * Review-only Realm Chat policy contract.
+ *
+ * This file does not grant client or server authority. The entry point must
+ * remain disabled until the owner/legal gate, server authority, moderation
+ * operations, release verification, and a separate activation record exist.
+ */
+export const WARPKEEP_REALM_CHAT_POLICY_VERSION =
+ '2026-08-03-realm-chat-policy-v1';
+
+export const WARPKEEP_REALM_CHAT_CHANNEL_KEY = 'realm:genesis-001';
+
+export const WARPKEEP_REALM_CHAT_CLIENT_ENTRY_ENABLED = false;
+
+export const WARPKEEP_REALM_CHAT_REVIEW_STATUS =
+ 'pending-owner-and-qualified-legal-review';
diff --git a/tests/activationToolingSecurity.test.ts b/tests/activationToolingSecurity.test.ts
index 48f4f1da..3e32de40 100644
--- a/tests/activationToolingSecurity.test.ts
+++ b/tests/activationToolingSecurity.test.ts
@@ -3362,13 +3362,13 @@ describe('activation publish safety', () => {
WARPKEEP_EXPECTED_ENABLED_ALLOWED_FID_COUNT: '3',
WARPKEEP_EXPECTED_FOUNDER_COUNT: '3',
WARPKEEP_EXPECTED_PLAYER_COUNT: '1',
- WARPKEEP_EXPECTED_TERMS_ACCEPTANCE_COUNT: '6',
+ WARPKEEP_EXPECTED_TERMS_ACCEPTANCE_COUNT: '7',
})).toThrow(/expectations were invalid/i);
expect(() => readFoundedPublishExpectations({
WARPKEEP_EXPECTED_ENABLED_ALLOWED_FID_COUNT: '100',
WARPKEEP_EXPECTED_FOUNDER_COUNT: '100',
WARPKEEP_EXPECTED_PLAYER_COUNT: '100',
- WARPKEEP_EXPECTED_TERMS_ACCEPTANCE_COUNT: '501',
+ WARPKEEP_EXPECTED_TERMS_ACCEPTANCE_COUNT: '601',
})).toThrow(/EXPECTED_TERMS_ACCEPTANCE_COUNT.*canonical integer/i);
expect(() => readFoundedPublishExpectations({
WARPKEEP_EXPECTED_ENABLED_ALLOWED_FID_COUNT: '5',
@@ -4976,7 +4976,7 @@ describe('protected aggregate child isolation', () => {
});
const completeEntryAgreementHistoryAggregate = Object.freeze({
...authenticatedGenesisV3FoundedAggregate,
- alphaTermsAcceptances: '5',
+ alphaTermsAcceptances: '6',
});
const genesisGenerationV3FoundedAggregate = Object.freeze({
...genesisV3FoundedAggregate,
@@ -5232,14 +5232,14 @@ describe('protected aggregate child isolation', () => {
PROTECTED_AGGREGATE_STAGE.GENESIS_V3_FOUNDED,
3,
1,
- 5,
+ 6,
)).not.toThrow();
expect(() => verifyExpectedAlphaV3Aggregate(
JSON.stringify(authenticatedGenesisV3FoundedAggregate),
PROTECTED_AGGREGATE_STAGE.GENESIS_V3_FOUNDED,
3,
1,
- 6,
+ 7,
)).toThrow(/entry-agreement row count was invalid/i);
});
@@ -5266,7 +5266,7 @@ describe('protected aggregate child isolation', () => {
it.each([
['player count', genesisV3FoundedAggregate, 4, 0],
- ['entry-agreement row count', authenticatedGenesisV3FoundedAggregate, 1, 6],
+ ['entry-agreement row count', authenticatedGenesisV3FoundedAggregate, 1, 7],
])('rejects an expected %s above its bounded aggregate limit', (_label, aggregate, players, terms) => {
expect(() => verifyExpectedAlphaV3Aggregate(
JSON.stringify(aggregate),
@@ -5866,7 +5866,7 @@ describe('protected aggregate child isolation', () => {
])).toThrow(/canonical integer/i);
});
- it.each(['-1', '00', '01', '+1', '1.0', '1e2', '501', 'abc', ''])
+ it.each(['-1', '00', '01', '+1', '1.0', '1e2', '601', 'abc', ''])
('rejects noncanonical or globally out-of-range entry-agreement counts: %j', value => {
expect(() => parseProductionVerifierArguments([
'--require-genesis-v3-founded-aggregate',
@@ -5906,28 +5906,28 @@ describe('protected aggregate child isolation', () => {
'--require-genesis-v3-founded-aggregate',
'--expected-founder-count=3',
'--expected-player-count=1',
- '--expected-terms-acceptance-count=5',
+ '--expected-terms-acceptance-count=6',
])).toMatchObject({
expectedFounderCount: 3,
expectedPlayerCount: 1,
- expectedTermsAcceptanceCount: 5,
+ expectedTermsAcceptanceCount: 6,
expectedEnabledAllowedFidCount: 3,
});
expect(() => parseProductionVerifierArguments([
'--require-genesis-v3-founded-aggregate',
'--expected-founder-count=3',
'--expected-player-count=1',
- '--expected-terms-acceptance-count=6',
+ '--expected-terms-acceptance-count=7',
])).toThrow(/supported immutable row history/i);
expect(parseProductionVerifierArguments([
'--require-genesis-v3-founded-aggregate',
'--expected-founder-count=100',
'--expected-player-count=100',
- '--expected-terms-acceptance-count=500',
+ '--expected-terms-acceptance-count=600',
])).toMatchObject({
expectedFounderCount: 100,
expectedPlayerCount: 100,
- expectedTermsAcceptanceCount: 500,
+ expectedTermsAcceptanceCount: 600,
expectedEnabledAllowedFidCount: 100,
});
});
diff --git a/tests/alphaTermsPolicy.test.ts b/tests/alphaTermsPolicy.test.ts
index 22e92d06..866842be 100644
--- a/tests/alphaTermsPolicy.test.ts
+++ b/tests/alphaTermsPolicy.test.ts
@@ -49,18 +49,20 @@ describe('versioned Alpha entry-agreement binding', () => {
expect(WARPKEEP_ENTRY_AGREEMENT_VERSION).toBe(MODULE_ENTRY_AGREEMENT_VERSION);
expect(WARPKEEP_ALPHA_TERMS_VERSION).toBe(WARPKEEP_ENTRY_AGREEMENT_VERSION);
expect(WARPKEEP_ENTRY_AGREEMENT_VERSION).toBe(
- '2026-07-31-hegemony-entry-agreement-v4',
+ '2026-08-03-hegemony-entry-agreement-v5',
);
expect(WARPKEEP_HEGEMONY_SOCIAL_CONTRACT_VERSION).toBe(
- '2026-07-19-HEGEMONY-SOCIAL-CONTRACT-V3',
+ '2026-08-03-HEGEMONY-SOCIAL-CONTRACT-V4',
);
expect(WARPKEEP_ENTRY_AGREEMENT_VERSION).toBe(
- '2026-07-31-hegemony-entry-agreement-v4',
+ '2026-08-03-hegemony-entry-agreement-v5',
);
expect(WARPKEEP_ENTRY_AGREEMENT_VERSION).not.toBe('2026-07-14');
});
it('keeps historical evidence distinct from the current entry/gameplay version', () => {
+ expect(WARPKEEP_HISTORICAL_ENTRY_AGREEMENT_VERSIONS)
+ .toContain('2026-07-31-hegemony-entry-agreement-v4');
expect(WARPKEEP_HISTORICAL_ENTRY_AGREEMENT_VERSIONS)
.toContain('2026-07-19-hegemony-entry-agreement-v3');
expect(WARPKEEP_HISTORICAL_ENTRY_AGREEMENT_VERSIONS).toContain('2026-07-14');
diff --git a/tests/hermesAdminSecurity.test.ts b/tests/hermesAdminSecurity.test.ts
index 4b4f8995..0c93c1aa 100644
--- a/tests/hermesAdminSecurity.test.ts
+++ b/tests/hermesAdminSecurity.test.ts
@@ -457,7 +457,7 @@ describe('Hermes machine-readable output', () => {
const status = foundedGenerationV2Status();
expect(verifyGenesisExpansionPreconditionV3(status)).toEqual(status);
- expect(WARPKEEP_ENTRY_AGREEMENT_ACCEPTANCE_RECORDS_PER_FID_MAXIMUM).toBe(5);
+ expect(WARPKEEP_ENTRY_AGREEMENT_ACCEPTANCE_RECORDS_PER_FID_MAXIMUM).toBe(6);
const retainedHistoryStatus = {
...status,
alphaTermsAcceptances: status.playersV2
diff --git a/tests/publicLegalDocuments.test.ts b/tests/publicLegalDocuments.test.ts
index 306dc946..8417e352 100644
--- a/tests/publicLegalDocuments.test.ts
+++ b/tests/publicLegalDocuments.test.ts
@@ -140,21 +140,37 @@ describe('public Alpha legal documents', () => {
expect(termsText).toContain('Warpkeep is open source');
expect(termsText).toContain('does not guarantee that a suggestion');
expect(termsText).toContain('Access is allowlist gated and conditional');
+ expect(termsText).toContain('If Realm Chat is later activated');
+ expect(termsText).toContain('retained as Realm history');
+ expect(termsText).toContain('warning is not required or guaranteed');
+ expect(termsText).toContain('Good-faith criticism');
+ expect(termsText).toContain('age and minor-participation policy is not final');
+ expect(termsText).toContain('qualified legal reviewer');
expect(termsText).not.toContain('tokens, points, airdrops');
});
- it('keeps the Hegemony covenant concise about fiction, conduct, and unfinished systems', () => {
+ it('keeps the Hegemony covenant explicit about fiction, conduct, moderation, and disabled chat', () => {
for (const expected of [
'Hegemony',
'Article II',
'Article III',
'Article VI',
+ 'Article VII',
'Honest counsel strengthens',
'Criticizing Warpkeep',
- 'Threats',
+ 'credible threats or incitement',
+ 'child sexual abuse material',
+ 'non-consensual intimate content',
+ 'unlawful hate or discriminatory abuse',
+ 'broad, good-faith, contextual judgment',
+ 'does not promise a formal appeal system',
+ 'false, retaliatory, or abusive reports',
+ 'Reporting must preserve the exact reported message',
+ 'local muting affects only',
+ 'disabled proposal pending owner and legal review',
'core strategy loop',
'Hegemony is game fiction',
- 'warn, limit, suspend, or revoke',
+ 'A warning is not required or guaranteed',
]) expect(socialContractText).toContain(expected);
for (const absent of ['Ouster', 'IP-level bans', 'Tribute is final']) {
expect(socialContractText).not.toContain(absent);
@@ -196,12 +212,22 @@ describe('public Alpha legal documents', () => {
WARPKEEP_HEGEMONY_SOCIAL_CONTRACT_VERSION,
WARPKEEP_ENTRY_AGREEMENT_VERSION,
'cryptographically binds the exact visible Terms and Social Contract texts',
+ 'Realm Chat is not active in this release',
+ 'message body, channel, server-owned sequence and time',
+ 'permanent archive, report records, and moderation reasons',
+ 'current browser session',
+ 'without routine gameplay expiry',
+ 'restricted, tombstoned, anonymised, or erased',
+ 'Reporters do not receive another player\'s private moderation outcome',
+ 'age and minor-participation policy is unresolved',
+ 'No age threshold is asserted by this draft',
]) expect(privacyText).toContain(expected);
expect(privacyText).toMatch(/FID.*entry.agreement.*accept(?:ed|ance).*(?:time|timestamp)/i);
expect(privacyText).toMatch(/not (?:this )?Privacy Notice.*blanket privacy consent/i);
expect(privacyText).not.toContain('world, player, faction');
expect(privacyText).not.toContain('tokens, points, airdrops');
+ expect(privacyText).not.toContain('Realm Chat is active');
});
it('keeps the narrow-screen retention table a labelled keyboard scroll region', () => {
diff --git a/tests/realmChatLegalPolicy.test.ts b/tests/realmChatLegalPolicy.test.ts
new file mode 100644
index 00000000..c1ed52e1
--- /dev/null
+++ b/tests/realmChatLegalPolicy.test.ts
@@ -0,0 +1,19 @@
+import { describe, expect, it } from 'vitest';
+
+import {
+ WARPKEEP_REALM_CHAT_CHANNEL_KEY,
+ WARPKEEP_REALM_CHAT_CLIENT_ENTRY_ENABLED,
+ WARPKEEP_REALM_CHAT_POLICY_VERSION,
+ WARPKEEP_REALM_CHAT_REVIEW_STATUS,
+} from '../src/legal/realmChatPolicy';
+
+describe('disabled Realm Chat legal contract', () => {
+ it('keeps the proposed social feature unavailable pending explicit review and activation', () => {
+ expect(WARPKEEP_REALM_CHAT_POLICY_VERSION)
+ .toBe('2026-08-03-realm-chat-policy-v1');
+ expect(WARPKEEP_REALM_CHAT_CHANNEL_KEY).toBe('realm:genesis-001');
+ expect(WARPKEEP_REALM_CHAT_CLIENT_ENTRY_ENABLED).toBe(false);
+ expect(WARPKEEP_REALM_CHAT_REVIEW_STATUS)
+ .toBe('pending-owner-and-qualified-legal-review');
+ });
+});
diff --git a/tests/warpkeepConnection.test.ts b/tests/warpkeepConnection.test.ts
index 55da907c..804367cf 100644
--- a/tests/warpkeepConnection.test.ts
+++ b/tests/warpkeepConnection.test.ts
@@ -1658,7 +1658,7 @@ describe('Warpkeep authenticated connection boundary', () => {
it('pins the browser and authoritative module to the same Terms version', () => {
expect(BROWSER_ALPHA_TERMS_VERSION).toBe(MODULE_ALPHA_TERMS_VERSION);
expect(BROWSER_ALPHA_TERMS_VERSION).toBe(
- '2026-07-31-hegemony-entry-agreement-v4'
+ '2026-08-03-hegemony-entry-agreement-v5'
);
});
From 24fa82afe80e6f56b681112da97cec924ef8e2c7 Mon Sep 17 00:00:00 2001
From: Ael
Date: Mon, 3 Aug 2026 14:05:58 +0200
Subject: [PATCH 2/2] Harden Realm Chat review rollout
---
CHANGELOG.md | 4 ++
docs/design/realm-chat-v1-contract.md | 13 ++++
...026-08-03-realm-chat-entry-agreement-v5.md | 14 ++++
scripts/entry-agreement-policy.d.mts | 2 +
scripts/entry-agreement-policy.mjs | 3 +
scripts/publish-spacetime-dev.d.mts | 4 ++
scripts/publish-spacetime-dev.mjs | 18 +++++
.../local-fullstack-browser-probe.mjs | 5 +-
.../qa-observer/local-fullstack-spacetime.mjs | 8 ++-
scripts/validate-pages-deploy-config.mjs | 65 +++++++++++++------
.../verify-spacetime-additive-migration.mjs | 15 ++---
spacetimedb/src/entryAgreementPolicy.ts | 4 ++
src/legal/alphaTermsPolicy.ts | 15 +++++
tests/activationToolingSecurity.test.ts | 14 ++++
tests/alphaTermsPolicy.test.ts | 26 ++++++++
tests/pagesDeployConfig.test.ts | 28 +++++++-
tests/publicLegalDocuments.test.ts | 18 +++++
tests/warpkeepConnection.test.ts | 4 ++
tests/workflowSecurity.test.ts | 4 ++
19 files changed, 230 insertions(+), 34 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7e6851b0..c7758c92 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -11,6 +11,10 @@ full engineering record.
moderation, history, and privacy boundaries. Chat remains disabled pending
owner and qualified legal review, server authority, release gates, and a
separate activation record; this draft collects no chat data.
+- Blocked independent production deployment of the review-only agreement on
+ both Pages and SpacetimeDB, pinned the proposed Privacy Notice text digest,
+ and removed duplicated agreement-version literals from migration and local
+ full-stack QA so rollout drift fails closed.
- Prepared an optional **Enable Admission Alerts** step after a confirmed access
request. It remains fail-closed behind a default-off release gate until the
owner-controlled signed notification canary is complete, and never affects
diff --git a/docs/design/realm-chat-v1-contract.md b/docs/design/realm-chat-v1-contract.md
index e6b3a12f..53d8df52 100644
--- a/docs/design/realm-chat-v1-contract.md
+++ b/docs/design/realm-chat-v1-contract.md
@@ -30,6 +30,15 @@ Merging this contract alone must not make a chat control visible or callable.
The client-side `false` constant is documentary defense in depth, not an
activation mechanism. Server channel state remains authoritative.
+The proposed V5 agreement is also marked `review-only-rollout-blocked`.
+Warpkeep's production Pages validator and canonical SpacetimeDB publisher must
+reject deployment/publication under that status. Read-only publisher dry runs
+remain available for compatibility evidence. A later reviewed rollout may change it to
+`production-approved` only when the exact legal approval and a coordinated
+browser/module compatibility proof are recorded. This prevents an automatic
+V5 Pages deployment from meeting a still-V4 production module and failing every
+player's entry-agreement handshake.
+
## V1 product boundary
- One admitted-Realm channel only: `realm:genesis-001`.
@@ -131,6 +140,10 @@ for individual rights: [data protection basics](https://www.edpb.europa.eu/sme/l
[lawful processing](https://www.edpb.europa.eu/sme/be-compliant/process-personal-data-lawfully_en),
and [individual rights](https://www.edpb.europa.eu/sme/be-compliant/respect-individuals-rights_ga).
+The exact normalized visible Privacy Notice text is integrity-pinned alongside
+the Terms and Social Contract even though the notice is not treated as blanket
+consent.
+
The age/minor-participation policy is intentionally unresolved. This contract
sets no age threshold. Activation is blocked until the owner and qualified legal
reviewer approve the applicable policy and any required parent/guardian, notice,
diff --git a/docs/legal/2026-08-03-realm-chat-entry-agreement-v5.md b/docs/legal/2026-08-03-realm-chat-entry-agreement-v5.md
index 17cb2a23..23637f6c 100644
--- a/docs/legal/2026-08-03-realm-chat-entry-agreement-v5.md
+++ b/docs/legal/2026-08-03-realm-chat-entry-agreement-v5.md
@@ -10,6 +10,12 @@ exact public documents:
`2026-08-03-HEGEMONY-SOCIAL-CONTRACT-V4`; and
- Privacy Notice revision `2026-08-03-v6` (notice only, not blanket consent).
+Normalized visible `` text digests proposed for review:
+
+- Terms: `dede7757c3be767b7a87e89e2c68817e9390cde91fabcf38246756afacdf51bd`;
+- Social Contract: `85941d066dd39f5be069d640f1419491e6fc0f691d01c292bfc3ed995c249110`;
+- Privacy Notice: `79bd17b795b399391ed0f6f84f2c7ff35fdb3ae64bf4ca17e3df0ad67d7b361f`.
+
The proposed agreement explains a future persistent Realm Chat, prohibited
high-risk conduct, contextual moderation, good-faith criticism, reports, local
mute, reconsideration, and the unresolved age/minor-participation policy. The
@@ -31,6 +37,14 @@ remains `false`. Chat activation requires the later PR sequence and a separate
activation record described in
[`docs/design/realm-chat-v1-contract.md`](../design/realm-chat-v1-contract.md).
+The candidate bundle remains `review-only-rollout-blocked`. Both the production
+Pages validator and canonical SpacetimeDB publisher must fail closed on live
+deployment/publication under that status; read-only publisher dry runs remain
+available for compatibility evidence. Promotion to `production-approved` requires a later reviewed change
+that records the exact approval and proves a coordinated browser/module rollout;
+otherwise V5 browser code could auto-deploy against a still-V4 module and deny
+Realm entry to every player.
+
## Required approval record
Before merge, the owner and qualified legal reviewer must explicitly resolve
diff --git a/scripts/entry-agreement-policy.d.mts b/scripts/entry-agreement-policy.d.mts
index e7443ea6..f2d904ea 100644
--- a/scripts/entry-agreement-policy.d.mts
+++ b/scripts/entry-agreement-policy.d.mts
@@ -1,5 +1,7 @@
export const WARPKEEP_ENTRY_AGREEMENT_VERSION:
'2026-08-03-hegemony-entry-agreement-v5';
+export const WARPKEEP_ENTRY_AGREEMENT_RELEASE_STATUS:
+ 'review-only-rollout-blocked';
export const WARPKEEP_HISTORICAL_ENTRY_AGREEMENT_VERSIONS: readonly [
'2026-07-31-hegemony-entry-agreement-v4',
'2026-07-19-hegemony-entry-agreement-v3',
diff --git a/scripts/entry-agreement-policy.mjs b/scripts/entry-agreement-policy.mjs
index d833f6bb..680b450b 100644
--- a/scripts/entry-agreement-policy.mjs
+++ b/scripts/entry-agreement-policy.mjs
@@ -6,6 +6,9 @@
export const WARPKEEP_ENTRY_AGREEMENT_VERSION =
'2026-08-03-hegemony-entry-agreement-v5';
+export const WARPKEEP_ENTRY_AGREEMENT_RELEASE_STATUS =
+ 'review-only-rollout-blocked';
+
export const WARPKEEP_HISTORICAL_ENTRY_AGREEMENT_VERSIONS = Object.freeze([
'2026-07-31-hegemony-entry-agreement-v4',
'2026-07-19-hegemony-entry-agreement-v3',
diff --git a/scripts/publish-spacetime-dev.d.mts b/scripts/publish-spacetime-dev.d.mts
index 11cdac56..5b5438f4 100644
--- a/scripts/publish-spacetime-dev.d.mts
+++ b/scripts/publish-spacetime-dev.d.mts
@@ -53,6 +53,10 @@ export function readFoundedPublishExpectations(
source?: Record,
): Readonly;
export function requireCanonicalPublishCoordinates(...args: any[]): any;
+export function requireEntryAgreementProductionRelease(
+ releaseStatus?: string,
+ dryRun?: boolean,
+): void;
export function runCurrentAdditiveMigrationProof(...args: any[]): any;
export function validateIssuerDeployment(...args: any[]): any;
export function verifyCanonicalDatabaseList(...args: any[]): any;
diff --git a/scripts/publish-spacetime-dev.mjs b/scripts/publish-spacetime-dev.mjs
index 766b0229..815ef43f 100644
--- a/scripts/publish-spacetime-dev.mjs
+++ b/scripts/publish-spacetime-dev.mjs
@@ -36,6 +36,7 @@ import {
} from './spacetime-table-schema-attestation.mjs';
import {
WARPKEEP_ENTRY_AGREEMENT_ACCEPTANCE_RECORDS_PER_FID_MAXIMUM,
+ WARPKEEP_ENTRY_AGREEMENT_RELEASE_STATUS,
} from './entry-agreement-policy.mjs';
import {
attestPinnedSpacetimeCli,
@@ -1206,6 +1207,19 @@ export function requireCanonicalPublishCoordinates(source = process.env) {
}
}
+export function requireEntryAgreementProductionRelease(
+ releaseStatus = WARPKEEP_ENTRY_AGREEMENT_RELEASE_STATUS,
+ dryRun = false,
+) {
+ if (dryRun === true) return;
+ if (releaseStatus !== 'production-approved') {
+ fail(
+ 'The current entry agreement is review-only; coordinated Pages and '
+ + 'SpacetimeDB rollout approval is required before production publication.',
+ );
+ }
+}
+
function validateFoundedPublishExpectations(value) {
if (
value === null
@@ -3937,6 +3951,10 @@ async function main() {
fail(`Set WARPKEEP_PUBLISH_CONFIRM=${database} after reviewing the target database; publish was not attempted.`);
}
const foundedExpectations = readFoundedPublishExpectations();
+ requireEntryAgreementProductionRelease(
+ WARPKEEP_ENTRY_AGREEMENT_RELEASE_STATUS,
+ dryRun,
+ );
// Remove the Hermes credential from the ambient environment before the
// long-running proof spawns any children. The bounded aggregate helpers
// receive it only through stdin and every child environment stays allowlisted.
diff --git a/scripts/qa-observer/local-fullstack-browser-probe.mjs b/scripts/qa-observer/local-fullstack-browser-probe.mjs
index d3868875..3f3c6d14 100644
--- a/scripts/qa-observer/local-fullstack-browser-probe.mjs
+++ b/scripts/qa-observer/local-fullstack-browser-probe.mjs
@@ -3,6 +3,9 @@ import { tmpdir } from 'node:os';
import { join, resolve } from 'node:path';
import { pathToFileURL } from 'node:url';
+import {
+ WARPKEEP_ENTRY_AGREEMENT_VERSION,
+} from '../entry-agreement-policy.mjs';
import {
DevtoolsPipeSession,
analyzeRenderedWebglPngScreenshot,
@@ -5209,7 +5212,7 @@ export async function runLocalFullstackBrowserProbe(options = {}) {
|| database.seedAttestation.workerCount !== 28
|| database.seedAttestation.entryAgreementAcceptedCurrent !== true
|| database.seedAttestation.entryAgreementRequiredVersion
- !== '2026-08-03-hegemony-entry-agreement-v5'
+ !== WARPKEEP_ENTRY_AGREEMENT_VERSION
|| database.seedAttestation.genericAssignments !== 0
|| database.seedAttestation.genericOccupations !== 0
|| database.seedAttestation.genericSchedules !== 0
diff --git a/scripts/qa-observer/local-fullstack-spacetime.mjs b/scripts/qa-observer/local-fullstack-spacetime.mjs
index 5b12dfa5..6d1ec09e 100644
--- a/scripts/qa-observer/local-fullstack-spacetime.mjs
+++ b/scripts/qa-observer/local-fullstack-spacetime.mjs
@@ -19,6 +19,9 @@ import { tmpdir } from 'node:os';
import { dirname, isAbsolute, join, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
+import {
+ WARPKEEP_ENTRY_AGREEMENT_VERSION,
+} from '../entry-agreement-policy.mjs';
import { attestPinnedSpacetimeCli } from '../spacetime-cli-attestation.mjs';
export const LOCAL_FULLSTACK_DATABASE = 'warpkeep-local-fullstack';
@@ -39,7 +42,6 @@ const SERVER_STOP_TIMEOUT_MILLISECONDS = 5_000;
const PROFILE_POLICY_VERSION = 'trusted-snapchain-profile-v3';
const RESOURCE_POLICY_VERSION = 'genesis-resource-yield-v1';
const WORKER_PROTOCOL_CAPABILITY = 'generic-castle-workers-v1';
-const ENTRY_AGREEMENT_VERSION = '2026-08-03-hegemony-entry-agreement-v5';
const LOCAL_FULLSTACK_FOUNDERS = Object.freeze(Array.from(
{ length: LOCAL_FULLSTACK_FOUNDER_COUNT },
(_, index) => Object.freeze({
@@ -777,7 +779,7 @@ async function seedLocalRealm(server, privateKey, moduleDigest) {
}
await callPlayer('bootstrap_player_v2');
await callPlayer('accept_alpha_terms_v1', JSON.stringify([
- ENTRY_AGREEMENT_VERSION,
+ WARPKEEP_ENTRY_AGREEMENT_VERSION,
true,
]));
const currentEntryAgreement = JSON.parse(
@@ -786,7 +788,7 @@ async function seedLocalRealm(server, privateKey, moduleDigest) {
if (
!Array.isArray(currentEntryAgreement)
|| currentEntryAgreement.length !== 2
- || currentEntryAgreement[0] !== ENTRY_AGREEMENT_VERSION
+ || currentEntryAgreement[0] !== WARPKEEP_ENTRY_AGREEMENT_VERSION
|| currentEntryAgreement[1] !== true
) {
fail('Disposable founder did not retain exact-current entry agreement authority.');
diff --git a/scripts/validate-pages-deploy-config.mjs b/scripts/validate-pages-deploy-config.mjs
index 7f928dd1..6647b750 100644
--- a/scripts/validate-pages-deploy-config.mjs
+++ b/scripts/validate-pages-deploy-config.mjs
@@ -1,3 +1,10 @@
+import { resolve } from 'node:path';
+import { pathToFileURL } from 'node:url';
+
+import {
+ WARPKEEP_ENTRY_AGREEMENT_RELEASE_STATUS,
+} from './entry-agreement-policy.mjs';
+
const EXPECTED_CANONICAL_ORIGIN = 'https://warpkeep.com';
const EXPECTED_REPOSITORY_URL = 'https://github.com/ael-dev3/Warpkeep';
const EXPECTED_AUDIENCE = 'warpkeep-spacetimedb';
@@ -44,62 +51,78 @@ function exactBoolean(value, label) {
fail(`${label} must be exactly true or false.`);
}
-function main() {
- if (process.env.DEPLOY_BASE !== '/') {
+export function validatePagesDeploymentConfiguration(
+ environment = process.env,
+ options = {},
+) {
+ const entryAgreementReleaseStatus =
+ options.entryAgreementReleaseStatus
+ ?? WARPKEEP_ENTRY_AGREEMENT_RELEASE_STATUS;
+ if (entryAgreementReleaseStatus !== 'production-approved') {
+ fail(
+ 'the current entry agreement is review-only; coordinated Pages and '
+ + 'SpacetimeDB rollout approval is required.',
+ );
+ }
+ if (environment.DEPLOY_BASE !== '/') {
fail('DEPLOY_BASE must be /.');
}
- if (process.env.VITE_WARPKEEP_RELEASE_CHANNEL !== 'alpha') {
+ if (environment.VITE_WARPKEEP_RELEASE_CHANNEL !== 'alpha') {
fail('VITE_WARPKEEP_RELEASE_CHANNEL must be alpha.');
}
- if (!SHA_PATTERN.test(process.env.VITE_WARPKEEP_BUILD_SHA ?? '')) {
+ if (!SHA_PATTERN.test(environment.VITE_WARPKEEP_BUILD_SHA ?? '')) {
fail('VITE_WARPKEEP_BUILD_SHA must be the full Git commit SHA.');
}
- if (process.env.VITE_WARPKEEP_REPOSITORY_URL !== EXPECTED_REPOSITORY_URL) {
+ if (environment.VITE_WARPKEEP_REPOSITORY_URL !== EXPECTED_REPOSITORY_URL) {
fail('VITE_WARPKEEP_REPOSITORY_URL must identify the Warpkeep repository.');
}
- if (process.env.VITE_WARPKEEP_CANONICAL_ORIGIN !== EXPECTED_CANONICAL_ORIGIN) {
+ if (environment.VITE_WARPKEEP_CANONICAL_ORIGIN !== EXPECTED_CANONICAL_ORIGIN) {
fail('VITE_WARPKEEP_CANONICAL_ORIGIN must be https://warpkeep.com.');
}
const sharedAlphaEnabled = exactBoolean(
- process.env.VITE_WARPKEEP_SHARED_ALPHA_ENABLED,
+ environment.VITE_WARPKEEP_SHARED_ALPHA_ENABLED,
'VITE_WARPKEEP_SHARED_ALPHA_ENABLED'
);
exactBoolean(
- process.env.VITE_WARPKEEP_ADMISSION_NOTIFICATIONS_ENABLED,
+ environment.VITE_WARPKEEP_ADMISSION_NOTIFICATIONS_ENABLED,
'VITE_WARPKEEP_ADMISSION_NOTIFICATIONS_ENABLED'
);
- if (process.env.VITE_WARPKEEP_OIDC_AUDIENCE !== EXPECTED_AUDIENCE) {
+ if (environment.VITE_WARPKEEP_OIDC_AUDIENCE !== EXPECTED_AUDIENCE) {
fail(`VITE_WARPKEEP_OIDC_AUDIENCE must be ${EXPECTED_AUDIENCE}.`);
}
- if (process.env.VITE_SPACETIMEDB_URI !== EXPECTED_SPACETIMEDB_URI) {
+ if (environment.VITE_SPACETIMEDB_URI !== EXPECTED_SPACETIMEDB_URI) {
fail(`VITE_SPACETIMEDB_URI must be ${EXPECTED_SPACETIMEDB_URI}.`);
}
- if (process.env.VITE_SPACETIMEDB_DATABASE !== EXPECTED_SPACETIMEDB_DATABASE) {
+ if (environment.VITE_SPACETIMEDB_DATABASE !== EXPECTED_SPACETIMEDB_DATABASE) {
fail(`VITE_SPACETIMEDB_DATABASE must be ${EXPECTED_SPACETIMEDB_DATABASE}.`);
}
if (!sharedAlphaEnabled) {
- console.log('Pages deployment validation passed with shared alpha disabled.');
- return;
+ return 'Pages deployment validation passed with shared alpha disabled.';
}
const bridge = exactHttpsOrigin(
- process.env.VITE_WARPKEEP_AUTH_BRIDGE_URL,
+ environment.VITE_WARPKEEP_AUTH_BRIDGE_URL,
'VITE_WARPKEEP_AUTH_BRIDGE_URL'
);
const issuer = exactHttpsOrigin(
- process.env.VITE_WARPKEEP_OIDC_ISSUER,
+ environment.VITE_WARPKEEP_OIDC_ISSUER,
'VITE_WARPKEEP_OIDC_ISSUER'
);
if (bridge !== EXPECTED_BRIDGE || issuer !== EXPECTED_BRIDGE) {
fail(`the bridge URL and OIDC issuer must both be ${EXPECTED_BRIDGE}.`);
}
- console.log('Pages deployment validation passed with shared alpha enabled.');
+ return 'Pages deployment validation passed with shared alpha enabled.';
}
-try {
- main();
-} catch (error) {
- console.error(error instanceof Error ? error.message : 'Pages deployment configuration is invalid.');
- process.exitCode = 1;
+const isEntrypoint = typeof process.argv[1] === 'string'
+ && import.meta.url === pathToFileURL(resolve(process.argv[1])).href;
+
+if (isEntrypoint) {
+ try {
+ console.log(validatePagesDeploymentConfiguration());
+ } catch (error) {
+ console.error(error instanceof Error ? error.message : 'Pages deployment configuration is invalid.');
+ process.exitCode = 1;
+ }
}
diff --git a/scripts/verify-spacetime-additive-migration.mjs b/scripts/verify-spacetime-additive-migration.mjs
index 24c6d794..3816d3c2 100644
--- a/scripts/verify-spacetime-additive-migration.mjs
+++ b/scripts/verify-spacetime-additive-migration.mjs
@@ -21,6 +21,10 @@ import {
import {
canonicalTableSchemaBoundaryDigest,
} from './spacetime-table-schema-attestation.mjs';
+import {
+ WARPKEEP_ENTRY_AGREEMENT_VERSION,
+ WARPKEEP_HISTORICAL_ENTRY_AGREEMENT_VERSIONS,
+} from './entry-agreement-policy.mjs';
const repositoryRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');
const fixtureModule = resolve(
@@ -103,14 +107,9 @@ const actualModuleOtherFid = 730_002;
// and is used only to prove the private v13 access-request lifecycle.
const syntheticMissingAccessRequestFid = '9007199254740991';
const syntheticSecondAccessRequestFid = '9007199254740990';
-const historicalEntryAgreementVersions = Object.freeze([
- '2026-07-31-hegemony-entry-agreement-v4',
- '2026-07-19-hegemony-entry-agreement-v3',
- '2026-07-19-hegemony-entry-agreement-v2',
- '2026-07-18-hegemony-entry-agreement-v1',
- '2026-07-14',
-]);
-const alphaTermsVersion = '2026-08-03-hegemony-entry-agreement-v5';
+const historicalEntryAgreementVersions =
+ WARPKEEP_HISTORICAL_ENTRY_AGREEMENT_VERSIONS;
+const alphaTermsVersion = WARPKEEP_ENTRY_AGREEMENT_VERSION;
const resourcePolicyVersion = 'genesis-resource-yield-v1';
const marksPolicyVersion = 'admitted-daily-mark-v1';
const profilePolicyVersion = 'trusted-snapchain-profile-v3';
diff --git a/spacetimedb/src/entryAgreementPolicy.ts b/spacetimedb/src/entryAgreementPolicy.ts
index a2a38a25..b98263bd 100644
--- a/spacetimedb/src/entryAgreementPolicy.ts
+++ b/spacetimedb/src/entryAgreementPolicy.ts
@@ -6,6 +6,10 @@
export const WARPKEEP_ENTRY_AGREEMENT_VERSION =
'2026-08-03-hegemony-entry-agreement-v5';
+/** Production publication remains blocked until the coordinated rollout PR. */
+export const WARPKEEP_ENTRY_AGREEMENT_RELEASE_STATUS =
+ 'review-only-rollout-blocked';
+
/** Compatibility alias retained by existing reducer and client imports. */
export const WARPKEEP_ALPHA_TERMS_VERSION = WARPKEEP_ENTRY_AGREEMENT_VERSION;
diff --git a/src/legal/alphaTermsPolicy.ts b/src/legal/alphaTermsPolicy.ts
index 65afaa58..0f58df76 100644
--- a/src/legal/alphaTermsPolicy.ts
+++ b/src/legal/alphaTermsPolicy.ts
@@ -10,6 +10,14 @@ export const WARPKEEP_HEGEMONY_SOCIAL_CONTRACT_VERSION =
export const WARPKEEP_ENTRY_AGREEMENT_VERSION =
'2026-08-03-hegemony-entry-agreement-v5';
+/**
+ * Review-only bundles must never reach either production surface independently.
+ * The later coordinated rollout PR must change this exact value only after the
+ * owner/legal gate and browser/module compatibility proof are complete.
+ */
+export const WARPKEEP_ENTRY_AGREEMENT_RELEASE_STATUS =
+ 'review-only-rollout-blocked';
+
/**
* Retained deployed reducer/input name. It identifies the complete linked
* entry agreement, not the Terms document alone.
@@ -27,3 +35,10 @@ export const WARPKEEP_ALPHA_TERMS_TEXT_SHA256 =
/** SHA-256 of the canonical Social Contract's normalized visible text. */
export const WARPKEEP_HEGEMONY_SOCIAL_CONTRACT_TEXT_SHA256 =
'85941d066dd39f5be069d640f1419491e6fc0f691d01c292bfc3ed995c249110';
+
+/** Factual notice version published alongside the proposed entry agreement. */
+export const WARPKEEP_ALPHA_PRIVACY_NOTICE_VERSION = '2026-08-03-v6';
+
+/** SHA-256 of the canonical Privacy Notice's normalized visible text. */
+export const WARPKEEP_ALPHA_PRIVACY_NOTICE_TEXT_SHA256 =
+ '79bd17b795b399391ed0f6f84f2c7ff35fdb3ae64bf4ca17e3df0ad67d7b361f';
diff --git a/tests/activationToolingSecurity.test.ts b/tests/activationToolingSecurity.test.ts
index 3e32de40..f324ba20 100644
--- a/tests/activationToolingSecurity.test.ts
+++ b/tests/activationToolingSecurity.test.ts
@@ -40,6 +40,7 @@ import {
publishModule,
readFoundedPublishExpectations,
requireCanonicalPublishCoordinates,
+ requireEntryAgreementProductionRelease,
runCurrentAdditiveMigrationProof,
validateIssuerDeployment,
verifyCanonicalDatabaseList,
@@ -3197,6 +3198,19 @@ describe('activation publish safety', () => {
})).not.toThrow();
});
+ it('blocks production publication while the entry agreement remains review-only', () => {
+ expect(() => requireEntryAgreementProductionRelease())
+ .toThrow(/entry agreement is review-only/i);
+ expect(() => requireEntryAgreementProductionRelease('production-approved'))
+ .not.toThrow();
+ expect(() => requireEntryAgreementProductionRelease(
+ 'review-only-rollout-blocked',
+ true,
+ )).not.toThrow();
+ expect(() => requireEntryAgreementProductionRelease(''))
+ .toThrow(/coordinated Pages and SpacetimeDB rollout approval/i);
+ });
+
it('binds the repair operator to one recent private successful publication receipt', async () => {
const root = await mkdtemp(join(
realpathSync(tmpdir()),
diff --git a/tests/alphaTermsPolicy.test.ts b/tests/alphaTermsPolicy.test.ts
index 866842be..f8cd1d33 100644
--- a/tests/alphaTermsPolicy.test.ts
+++ b/tests/alphaTermsPolicy.test.ts
@@ -5,8 +5,11 @@ import { fileURLToPath } from 'node:url';
import { describe, expect, it } from 'vitest';
import {
+ WARPKEEP_ALPHA_PRIVACY_NOTICE_TEXT_SHA256,
+ WARPKEEP_ALPHA_PRIVACY_NOTICE_VERSION,
WARPKEEP_ALPHA_TERMS_TEXT_SHA256,
WARPKEEP_ALPHA_TERMS_VERSION,
+ WARPKEEP_ENTRY_AGREEMENT_RELEASE_STATUS,
WARPKEEP_ENTRY_AGREEMENT_VERSION,
WARPKEEP_HEGEMONY_SOCIAL_CONTRACT_TEXT_SHA256,
WARPKEEP_HEGEMONY_SOCIAL_CONTRACT_VERSION,
@@ -18,11 +21,13 @@ import {
import {
WARPKEEP_ENTRY_AGREEMENT_ACCEPTANCE_RECORDS_PER_FID_MAXIMUM,
WARPKEEP_ENTRY_AGREEMENT_EVIDENCE_VERSIONS,
+ WARPKEEP_ENTRY_AGREEMENT_RELEASE_STATUS as MODULE_ENTRY_AGREEMENT_RELEASE_STATUS,
WARPKEEP_HISTORICAL_ENTRY_AGREEMENT_VERSIONS,
} from '../spacetimedb/src/entryAgreementPolicy';
import {
WARPKEEP_ENTRY_AGREEMENT_ACCEPTANCE_RECORDS_PER_FID_MAXIMUM as TOOLING_ENTRY_AGREEMENT_ACCEPTANCE_RECORDS_PER_FID_MAXIMUM,
WARPKEEP_ENTRY_AGREEMENT_EVIDENCE_VERSIONS as TOOLING_ENTRY_AGREEMENT_EVIDENCE_VERSIONS,
+ WARPKEEP_ENTRY_AGREEMENT_RELEASE_STATUS as TOOLING_ENTRY_AGREEMENT_RELEASE_STATUS,
WARPKEEP_ENTRY_AGREEMENT_VERSION as TOOLING_ENTRY_AGREEMENT_VERSION,
WARPKEEP_HISTORICAL_ENTRY_AGREEMENT_VERSIONS as TOOLING_HISTORICAL_ENTRY_AGREEMENT_VERSIONS,
} from '../scripts/entry-agreement-policy.mjs';
@@ -35,6 +40,10 @@ const socialContractHtml = readFileSync(
resolve(dirname(fileURLToPath(import.meta.url)), '../public/social-contract/index.html'),
'utf8'
);
+const privacyHtml = readFileSync(
+ resolve(dirname(fileURLToPath(import.meta.url)), '../public/privacy/index.html'),
+ 'utf8'
+);
function normalizedPublicDocumentText(html: string, documentName: string) {
const parsedDocument = new DOMParser().parseFromString(html, 'text/html');
@@ -58,6 +67,13 @@ describe('versioned Alpha entry-agreement binding', () => {
'2026-08-03-hegemony-entry-agreement-v5',
);
expect(WARPKEEP_ENTRY_AGREEMENT_VERSION).not.toBe('2026-07-14');
+ expect(WARPKEEP_ENTRY_AGREEMENT_RELEASE_STATUS)
+ .toBe('review-only-rollout-blocked');
+ expect(MODULE_ENTRY_AGREEMENT_RELEASE_STATUS)
+ .toBe(WARPKEEP_ENTRY_AGREEMENT_RELEASE_STATUS);
+ expect(TOOLING_ENTRY_AGREEMENT_RELEASE_STATUS)
+ .toBe(WARPKEEP_ENTRY_AGREEMENT_RELEASE_STATUS);
+ expect(WARPKEEP_ALPHA_PRIVACY_NOTICE_VERSION).toBe('2026-08-03-v6');
});
it('keeps historical evidence distinct from the current entry/gameplay version', () => {
@@ -104,6 +120,16 @@ describe('versioned Alpha entry-agreement binding', () => {
WARPKEEP_HEGEMONY_SOCIAL_CONTRACT_TEXT_SHA256,
[WARPKEEP_HEGEMONY_SOCIAL_CONTRACT_VERSION],
],
+ [
+ 'Privacy Notice',
+ privacyHtml,
+ WARPKEEP_ALPHA_PRIVACY_NOTICE_TEXT_SHA256,
+ [
+ WARPKEEP_ALPHA_PRIVACY_NOTICE_VERSION,
+ WARPKEEP_ENTRY_AGREEMENT_VERSION,
+ WARPKEEP_HEGEMONY_SOCIAL_CONTRACT_VERSION,
+ ],
+ ],
] as const)(
'fails when canonical visible %s wording drifts without policy review',
(documentName, html, expectedDigest, requiredVersions) => {
diff --git a/tests/pagesDeployConfig.test.ts b/tests/pagesDeployConfig.test.ts
index 4af4b6cb..5c768456 100644
--- a/tests/pagesDeployConfig.test.ts
+++ b/tests/pagesDeployConfig.test.ts
@@ -2,6 +2,9 @@ import { spawnSync } from 'node:child_process';
import { describe, expect, it } from 'vitest';
+// @ts-expect-error Repository JavaScript scripts intentionally expose test hooks.
+import { validatePagesDeploymentConfiguration } from '../scripts/validate-pages-deploy-config.mjs';
+
const FULL_SHA = 'abcdef0123456789abcdef0123456789abcdef01';
function deploymentEnvironment(overrides: Record = {}) {
@@ -23,7 +26,7 @@ function deploymentEnvironment(overrides: Record = {}) {
};
}
-function validate(overrides?: Record) {
+function validateCli(overrides?: Record) {
return spawnSync(process.execPath, ['scripts/validate-pages-deploy-config.mjs'], {
cwd: process.cwd(),
encoding: 'utf8',
@@ -31,7 +34,30 @@ function validate(overrides?: Record) {
});
}
+function validate(overrides?: Record) {
+ try {
+ const stdout = validatePagesDeploymentConfiguration(
+ deploymentEnvironment(overrides),
+ { entryAgreementReleaseStatus: 'production-approved' },
+ );
+ return { status: 0, stdout, stderr: '' };
+ } catch (error) {
+ return {
+ status: 1,
+ stdout: '',
+ stderr: error instanceof Error ? error.message : String(error),
+ };
+ }
+}
+
describe('Pages deployment configuration validation', () => {
+ it('blocks the review-only agreement from the real deployment entry point', () => {
+ const result = validateCli();
+ expect(result.status).not.toBe(0);
+ expect(result.stderr).toContain('entry agreement is review-only');
+ expect(result.stderr).toContain('coordinated Pages and SpacetimeDB rollout approval');
+ });
+
it('accepts the root-base canonical build with shared alpha deliberately disabled', () => {
const result = validate();
expect(result.status).toBe(0);
diff --git a/tests/publicLegalDocuments.test.ts b/tests/publicLegalDocuments.test.ts
index 8417e352..252a22ef 100644
--- a/tests/publicLegalDocuments.test.ts
+++ b/tests/publicLegalDocuments.test.ts
@@ -2,6 +2,7 @@ import { readFileSync } from 'node:fs';
import { describe, expect, it } from 'vitest';
import {
+ WARPKEEP_ALPHA_PRIVACY_NOTICE_VERSION,
WARPKEEP_ENTRY_AGREEMENT_VERSION,
WARPKEEP_HEGEMONY_SOCIAL_CONTRACT_VERSION,
} from '../src/legal/alphaTermsPolicy';
@@ -19,6 +20,16 @@ const legalCss = readFileSync('public/legal/warpkeep-legal.css', 'utf8');
const strictPublicLegalCsp =
"default-src 'none'; style-src 'self'; base-uri 'none'; form-action 'none'";
+const exactPublicLegalHrefAllowlist = new Set([
+ '../',
+ './index.html',
+ '../terms/index.html',
+ '../social-contract/index.html',
+ '../privacy/index.html',
+ 'https://github.com/ael-dev3',
+ 'https://github.com/ael-dev3/Warpkeep',
+ 'https://github.com/ael-dev3/Warpkeep/security/policy',
+]);
function parse(html: string) {
return new DOMParser().parseFromString(html, 'text/html');
@@ -110,6 +121,12 @@ describe('public Alpha legal documents', () => {
expect(link.rel.split(/\s+/)).toEqual(expect.arrayContaining(['noopener', 'noreferrer']));
}
+ for (const link of document.querySelectorAll('a[href]')) {
+ expect(exactPublicLegalHrefAllowlist.has(link.getAttribute('href') ?? '')).toBe(true);
+ expect(link.hasAttribute('download')).toBe(false);
+ expect(link.hasAttribute('ping')).toBe(false);
+ }
+
for (const asset of document.querySelectorAll(
'img, source, video, audio, script, iframe, object, embed',
)) {
@@ -211,6 +228,7 @@ describe('public Alpha legal documents', () => {
'Hegemony Social Contract',
WARPKEEP_HEGEMONY_SOCIAL_CONTRACT_VERSION,
WARPKEEP_ENTRY_AGREEMENT_VERSION,
+ WARPKEEP_ALPHA_PRIVACY_NOTICE_VERSION,
'cryptographically binds the exact visible Terms and Social Contract texts',
'Realm Chat is not active in this release',
'message body, channel, server-owned sequence and time',
diff --git a/tests/warpkeepConnection.test.ts b/tests/warpkeepConnection.test.ts
index 804367cf..82dc422e 100644
--- a/tests/warpkeepConnection.test.ts
+++ b/tests/warpkeepConnection.test.ts
@@ -1215,6 +1215,10 @@ describe('Warpkeep authenticated connection boundary', () => {
{
requiredVersion: '2026-07-19-hegemony-entry-agreement-v2',
acceptedCurrent: true
+ },
+ {
+ requiredVersion: '2026-07-31-hegemony-entry-agreement-v4',
+ acceptedCurrent: true
}
])('rejects malformed or mismatched entry-agreement status %#', async raw => {
const connection = {
diff --git a/tests/workflowSecurity.test.ts b/tests/workflowSecurity.test.ts
index a5c41b74..dd0dcd2d 100644
--- a/tests/workflowSecurity.test.ts
+++ b/tests/workflowSecurity.test.ts
@@ -89,6 +89,10 @@ describe('GitHub workflow security policy', () => {
expect(build).toContain(
'WARPKEEP_SHARED_ALPHA_ENABLED must be exactly true or false.',
);
+ expect(build).toContain('npm run validate:pages-config');
+ expect(build.indexOf('npm run validate:pages-config')).toBeLessThan(
+ build.indexOf('npm run build'),
+ );
expect(source).toContain('group: pages-main');
expect(source).not.toMatch(/^\s+group:\s*pages\s*$/m);
});