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
3 changes: 2 additions & 1 deletion deploy/homebrew/bin/memex-local
Original file line number Diff line number Diff line change
Expand Up @@ -347,7 +347,8 @@ cmd_registry() {
info " memex-local registry $url --key mwr_…"
fi
info "Apply it: memex-local update (pulls the CI-built image from $ACR and re-renders the chart;"
info " the portal registers itself on first boot and installs what it is granted)."
info " after a platform admin accepts the privacy statement and platform terms in Settings ▸
Instance registration, the portal registers itself and installs what it is granted)."
;;
esac
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,10 @@ private static Task<IResult> Register(
body.BootstrapKey, body.InstanceId, body.DisplayName, body.Description, body.HomeUrl))
.Select(registration => (IResult)Results.Json(
new InstanceRegistrationPayloads.Response(
registration.Instance.InstanceId, registration.RawKey),
registration.Instance.InstanceId, registration.RawKey)
{
Plan = registration.Instance.Plan ?? PlanTierRanks.BaselinePlan,
},
InstanceRegistrationPayloads.Json))
.Catch((InvalidBootstrapKeyException ex) =>
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
Name: An installation asks before it registers
Category: Feature
Description: A new installation registers itself at the plugin registry only after a platform admin has read and accepted the privacy statement and the platform terms in Settings ▸ Instance registration — then it registers, obtains its token and shows the catalogue its plan covers
Icon: Sparkle
Order: -20260830
---

# An installation asks before it registers

A new installation that is configured to register itself at a plugin registry without a key — the Homebrew default — no longer does so silently on first start. It starts, and asks: a platform admin opens **Settings ▸ Instance registration**, reads the privacy statement and the platform terms shown there, accepts both, and registers. The consent is recorded on the installation (who, when, which version of each text); only then does the installation register, obtain its short-lived token and read the package catalogue its plan covers — which the same page shows, together with the instance id and the plan it landed on (free, unless the registry's admins raise it). Withdrawing the consent from the same page stops the installation from registering again. Installations provisioned with a registration key by an operator are unaffected.
36 changes: 33 additions & 3 deletions src/MeshWeaver.Graph/Configuration/MeshWeaverInstanceNodeType.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,21 @@ public static class MeshWeaverInstanceNodeType
/// <see cref="IndexNamespace"/> with instance-key indexes — content type tells them apart.</summary>
public const string RegistrationKeyNodeType = "RegistrationKey";

/// <summary>The node-type identifier for the CONSUMER's consent record
/// (<see cref="InstanceConsent"/>) — the privacy statement and platform terms a platform admin
/// of this installation accepted before it registers itself at a registry.</summary>
public const string ConsentNodeType = "InstanceConsent";

/// <summary>The consent record's namespace: the Admin partition, so only a global admin can
/// give (or withdraw, by deleting it) the deployment's consent.</summary>
public const string ConsentNamespace = "Admin";

/// <summary>The consent record's id — one per instance.</summary>
public const string ConsentId = "InstanceConsent";

/// <summary>Path of the consent record.</summary>
public const string ConsentPath = $"{ConsentNamespace}/{ConsentId}";

/// <summary>
/// Registers both node types on the mesh builder and puts their content types in the hub's
/// type registry so they serialize across silos (without this, a cross-silo create fails with
Expand All @@ -53,20 +68,35 @@ public static class MeshWeaverInstanceNodeType
public static TBuilder AddMeshWeaverInstanceType<TBuilder>(this TBuilder builder)
where TBuilder : MeshBuilder
{
builder.AddMeshNodes(CreateMeshNode(), CreateGrantMeshNode(), CreateRegistrationKeyMeshNode());
builder.AddMeshNodes(
CreateMeshNode(), CreateGrantMeshNode(), CreateRegistrationKeyMeshNode(), CreateConsentMeshNode());
// An instance is infrastructure identity, not content — keep all of these out of
// autocomplete so they never surface as pickable nodes in the composer.
builder.AddAutocompleteExcludedTypes(NodeType, GrantNodeType, RegistrationKeyNodeType);
builder.AddAutocompleteExcludedTypes(NodeType, GrantNodeType, RegistrationKeyNodeType, ConsentNodeType);
builder.ConfigureHub(config => config
.WithType<MeshWeaverInstance>(nameof(MeshWeaverInstance))
.WithType<MeshWeaverInstanceIndex>(nameof(MeshWeaverInstanceIndex))
.WithType<PluginGrant>(nameof(PluginGrant))
.WithType<PluginGrantEntry>(nameof(PluginGrantEntry))
.WithType<RegistrationKey>(nameof(RegistrationKey))
.WithType<RegistrationKeyIndex>(nameof(RegistrationKeyIndex)));
.WithType<RegistrationKeyIndex>(nameof(RegistrationKeyIndex))
.WithType<InstanceConsent>(nameof(InstanceConsent)));
return builder;
}

/// <summary>The <see cref="InstanceConsent"/> node definition. One record in the Admin
/// partition; the partition's access control is what keeps everyone but a global admin from
/// consenting on the deployment's behalf.</summary>
public static MeshNode CreateConsentMeshNode() => new(ConsentNodeType)
{
Name = "Instance Consent",
IsSatelliteType = false,
ExcludeFromContext = new HashSet<string> { "search", "create", "content" },
HubConfiguration = config => config
.AddMeshDataSource(source => source
.WithContentType<InstanceConsent>())
};

/// <summary>The <see cref="MeshWeaverInstance"/> node definition (instances + their index).</summary>
public static MeshNode CreateMeshNode() => new(NodeType)
{
Expand Down
53 changes: 53 additions & 0 deletions src/MeshWeaver.Mesh.Contract/Security/InstanceConsent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
namespace MeshWeaver.Mesh.Security;

/// <summary>
/// The consent that gates an installation's OPEN registration at a registry: a platform admin of
/// this instance has read the privacy statement and the platform terms and accepted both, on a
/// date, under a name. Until this record exists the instance does not register itself, does not
/// obtain a token, and does not pull a catalogue — it starts, and asks.
///
/// <para>One record per instance, at <c>Admin/InstanceConsent</c>: consent is a property of the
/// DEPLOYMENT, given once by someone entitled to speak for it (a global admin), not of a user. The
/// texts are recorded by HASH so an audit can tell WHICH statement and WHICH terms were accepted
/// after either has been edited; a later edit does not revoke the consent — the operator who
/// changes the terms decides whether to ask again, by deleting this record.</para>
///
/// <para>Only OPEN registration (no bootstrap key) is gated. An instance provisioned with a
/// registration key was set up by an operator who accepted the terms on the fleet's side; asking
/// the same question of an unattended pod would leave it un-registered forever.</para>
/// </summary>
public record InstanceConsent
{
/// <summary>The instance id the consent was given for — the one the registration will claim.</summary>
public string InstanceId { get; init; } = "";

/// <summary>The registry the instance registers at.</summary>
public string RegistryUrl { get; init; } = "";

/// <summary>SHA-256 hex of the privacy statement as shown when it was accepted.</summary>
public string PrivacyStatementHash { get; init; } = "";

/// <summary>SHA-256 hex of the platform terms as shown when they were accepted.</summary>
public string TermsHash { get; init; } = "";

/// <summary>When both were accepted.</summary>
public DateTimeOffset AcceptedAt { get; init; }

/// <summary>ObjectId of the global admin who accepted.</summary>
public string AcceptedByUserId { get; init; } = "";

/// <summary>Display name of the accepting admin.</summary>
public string AcceptedByName { get; init; } = "";

/// <summary>Email of the accepting admin.</summary>
public string AcceptedByEmail { get; init; } = "";

/// <summary>Whether this record carries a complete consent — both texts accepted by a named
/// principal. An incomplete record gates exactly like an absent one.</summary>
public bool IsComplete =>
!string.IsNullOrWhiteSpace(InstanceId)
&& !string.IsNullOrWhiteSpace(PrivacyStatementHash)
&& !string.IsNullOrWhiteSpace(TermsHash)
&& !string.IsNullOrWhiteSpace(AcceptedByUserId)
&& AcceptedAt != default;
}
24 changes: 24 additions & 0 deletions src/MeshWeaver.Messaging.Hub/Localization/strings.de.json
Original file line number Diff line number Diff line change
Expand Up @@ -989,6 +989,30 @@
"instancePlan.promote": "Plan setzen",
"instancePlan.promoted": "Plan gesetzt:",
"instancePlan.error.unknownInstance": "Keine registrierte Instanz trägt diese ID.",
"instanceRegistration.title": "Instanzregistrierung",
"instanceRegistration.intro": "Diese Installation registriert sich bei einer Plugin-Registry, um ihre Identität, ein kurzlebiges Token und den Paketkatalog ihres Plans zu erhalten. Das geschieht erst, nachdem ein Plattform-Admin die Datenschutzerklärung und die Plattformbedingungen unten gelesen und akzeptiert hat.",
"instanceRegistration.notConfigured": "Für diese Installation ist keine Registry konfiguriert (PluginCatalog:RegistryUrl / InstanceId). Nichts zu registrieren.",
"instanceRegistration.keyed": "Diese Installation registriert sich mit einem vom Betreiber bereitgestellten Registrierungsschlüssel; ein Zustimmungsschritt entfällt.",
"instanceRegistration.awaitingConsent": "Zustimmung ausstehend — diese Installation ist noch nicht registriert.",
"instanceRegistration.registered": "Registriert",
"instanceRegistration.plan": "Plan",
"instanceRegistration.registry": "Registry",
"instanceRegistration.instanceId": "Instanz-ID",
"instanceRegistration.consentGivenBy": "Zustimmung erteilt von",
"instanceRegistration.packages": "Für diese Installation verfügbare Pakete",
"instanceRegistration.packagesLoading": "Katalog wird gelesen…",
"instanceRegistration.packagesFailed": "Der Katalog konnte nicht gelesen werden:",
"instanceRegistration.privacyHeading": "Datenschutzerklärung",
"instanceRegistration.termsHeading": "Plattformbedingungen",
"instanceRegistration.acceptPrivacy": "Ich habe die Datenschutzerklärung gelesen und akzeptiere sie",
"instanceRegistration.acceptTerms": "Ich habe die Plattformbedingungen gelesen und akzeptiere sie",
"instanceRegistration.register": "Diese Installation registrieren",
"instanceRegistration.registering": "Zustimmung erfasst — Registrierung läuft…",
"instanceRegistration.error.acceptBoth": "Akzeptieren Sie sowohl die Datenschutzerklärung als auch die Plattformbedingungen, um zu registrieren.",
"instanceRegistration.error.failed": "Die Zustimmung konnte nicht erfasst werden:",
"instanceRegistration.withdraw": "Zustimmung zurückziehen",
"instanceRegistration.withdrawn": "Zustimmung zurückgezogen — die Installation registriert sich nicht mehr; löschen Sie die gespeicherten Zugangsdaten, um die Authentifizierung zu beenden.",
"instanceRegistration.missingPrivacy": "Auf dieser Installation ist noch keine Datenschutzerklärung veröffentlicht — veröffentlichen Sie zuerst eine unter Einstellungen ▸ Datenschutz. Sie wird öffentlich unter /privacy bereitgestellt.",
"menu.userProfile": "Benutzerprofil",
"chat.chat": "Chat",
"chat.recentThreads": "Letzte Threads",
Expand Down
24 changes: 24 additions & 0 deletions src/MeshWeaver.Messaging.Hub/Localization/strings.en.json
Original file line number Diff line number Diff line change
Expand Up @@ -989,6 +989,30 @@
"instancePlan.promote": "Set plan",
"instancePlan.promoted": "Plan set:",
"instancePlan.error.unknownInstance": "No registered instance carries that ID.",
"instanceRegistration.title": "Instance registration",
"instanceRegistration.intro": "This installation registers itself at a plugin registry to obtain its identity, a short-lived token and the package catalogue its plan covers. It does so only after a platform admin has read and accepted the privacy statement and the platform terms below.",
"instanceRegistration.notConfigured": "No registry is configured for this installation (PluginCatalog:RegistryUrl / InstanceId). Nothing to register.",
"instanceRegistration.keyed": "This installation registers with a registration key its operator provisioned; no consent step applies here.",
"instanceRegistration.awaitingConsent": "Awaiting consent — this installation has not registered yet.",
"instanceRegistration.registered": "Registered",
"instanceRegistration.plan": "Plan",
"instanceRegistration.registry": "Registry",
"instanceRegistration.instanceId": "Instance ID",
"instanceRegistration.consentGivenBy": "Consent given by",
"instanceRegistration.packages": "Packages available to this installation",
"instanceRegistration.packagesLoading": "Reading the catalogue…",
"instanceRegistration.packagesFailed": "Could not read the catalogue:",
"instanceRegistration.privacyHeading": "Privacy statement",
"instanceRegistration.termsHeading": "Platform terms",
"instanceRegistration.acceptPrivacy": "I have read and accept the privacy statement",
"instanceRegistration.acceptTerms": "I have read and accept the platform terms",
"instanceRegistration.register": "Register this installation",
"instanceRegistration.registering": "Consent recorded — registering…",
"instanceRegistration.error.acceptBoth": "Accept both the privacy statement and the platform terms to register.",
"instanceRegistration.error.failed": "Could not record the consent:",
"instanceRegistration.withdraw": "Withdraw consent",
"instanceRegistration.withdrawn": "Consent withdrawn — the installation stops registering; delete the stored credential to stop authenticating.",
"instanceRegistration.missingPrivacy": "No privacy statement has been published on this installation yet — publish one under Settings ▸ Privacy before registering. It is served publicly at /privacy.",
"menu.userProfile": "User profile",
"chat.chat": "Chat",
"chat.recentThreads": "Recent threads",
Expand Down
Loading
Loading