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
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
using System.Collections.Concurrent;
using System.Reactive.Linq;
using MeshWeaver.AI.Connect;
using MeshWeaver.Mesh.Services;
using MeshWeaver.Hosting.AspNetCore;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
Expand Down
8 changes: 6 additions & 2 deletions memex/Memex.Portal.Shared/Memex.Portal.Shared.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -101,9 +101,13 @@
the modules/<Name>/ closure layout in MeshModulesPublish.targets and activates via
Modules:Assemblies -> MarkdownExportProviderAttribute. Only the wire contract stays in
the app closure (via MeshWeaver.Blazor -> MeshWeaver.Markdown.Export.Contract). -->
<ProjectReference Include="..\..\src\MeshWeaver.AI\MeshWeaver.AI.csproj" />
<ProjectReference Include="..\..\src\MeshWeaver.Mesh.Operations\MeshWeaver.Mesh.Operations.csproj" />
<!-- The AI provider packs — MeshWeaver.AI.OpenAI, .AzureFoundry, .ClaudeCode, .Copilot — are
<!-- 🚨 MeshWeaver.AI is deliberately NOT referenced (#2276): the engine AND its maintenance
application ship as ONE Store entry and activate via Modules:Assemblies ->
AiMeshModuleAttribute, which reads this deployment's own configuration to decide what to
register. A reference here would put the DLL back in the app closure, where the module
landing refuses the same-identity module with a 409 on every instance.
The AI provider packs — MeshWeaver.AI.OpenAI, .AzureFoundry, .ClaudeCode, .Copilot — are
deliberately NOT referenced (#1644 step 2): they ship via the modules/<Name>/ closure
layout in MeshModulesPublish.targets and activate via Modules:Assemblies -> each pack's
MeshNodeProviderAttribute (a deployment drops a provider by editing its module list).
Expand Down
83 changes: 15 additions & 68 deletions memex/Memex.Portal.Shared/MemexConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
using Memex.Portal.Shared.Settings;
using Memex.Portal.Shared.Social;
using Microsoft.Extensions.DependencyInjection.Extensions;
using MeshWeaver.AI;
using MeshWeaver.Hosting.AspNetCore.Portal;
using MeshWeaver.Hosting.Grpc;
using MeshWeaver.Blazor.Pages;
Expand Down Expand Up @@ -237,31 +236,14 @@ public static void ConfigureMemexServices(this WebApplicationBuilder builder)
// 404 and the reply sender self-skips at ApplicationStarted.)

// Shared on-disk WORKSPACE dir the agent→skill sync maintains (.claude/skills + AGENTS.md); both
// CLI harnesses set it as the session's working directory so every session sees the MeshWeaver
// agents/skills + the mesh-is-via-MCP base instructions. Defaults to a sibling of the per-user
// .claude root (e.g. /mnt/users → /mnt/users/_skills) when not explicitly configured.
var skillsDir = builder.Configuration["Skills:Directory"];
if (string.IsNullOrWhiteSpace(skillsDir))
{
var claudeRoot = builder.Configuration["ClaudeCode:ConfigDirRoot"]?.TrimEnd('/', '\\');
skillsDir = string.IsNullOrEmpty(claudeRoot) ? null : $"{claudeRoot}/_skills";
}

// The ClaudeCode/Copilot packs bind their own options (incl. the SkillsDirectory
// derivation) from configuration when loaded via Modules:Assemblies.

// Reactive skill→file sync: writes AGENTS.md (the base "mesh-is-via-MCP" instructions + a LISTING
// of the platform nodeType:Skill catalog — name, description, load path) to the shared volume and
// keeps it in sync as skill nodes change (observable query). Skill BODIES are never written to
// disk — the harness reads each on demand via the meshweaver MCP `get`. Runs for the process lifetime.
if ((features.Ai.Clis.ClaudeCode || features.Ai.Clis.Copilot) && !string.IsNullOrWhiteSpace(skillsDir))
{
services.Configure<Skills.AgentSkillSyncOptions>(o => o.Directory = skillsDir);
services.AddHostedService<Skills.AgentSkillSyncService>();
}
// The reactive skill→file sync for the co-hosted CLIs rides the AI module, which reads
// Skills:Directory / ClaudeCode:ConfigDirRoot itself (AiMeshModuleAttribute).

// Register the AI chat services (must be after all factory registrations)
services.AddAgentChatServices();
// The AI chat services are registered by AddAI() inside the module.

// (The WebSearch agent tools ride the MeshWeaver.AI.WebSearch module — listed under
// Modules:Assemblies, binding its own WebSearch configuration section. Agent plugins
Expand Down Expand Up @@ -290,10 +272,7 @@ public static void ConfigureMemexServices(this WebApplicationBuilder builder)
?? "Memex"));
// Automatic, token-based MCP back-connection for the co-hosted Claude Code / Copilot CLIs.
// The chat clients resolve this at spawn to mint/reuse the per-user MCP ApiToken + URL.
services.AddSingleton<MeshWeaver.AI.Connect.IMcpBackConnection, McpBackConnectionService>();
// ModelProviderService backs the Models settings tab — users store
// their own AI provider credentials as MeshNodes in their namespace.
services.AddSingleton<Memex.Portal.Shared.Models.ModelProviderService>();
services.AddSingleton<MeshWeaver.Mesh.Services.IMcpBackConnection, McpBackConnectionService>();
// ProviderModelLister moved to MeshWeaver.AI (AddAgentChatServices registers it) — the
// add-provider flow and the OpenAI module's model-discovery sync both resolve it there.
// OpenAI-compatible (Ollama) model auto-discovery rides the MeshWeaver.AI.OpenAI MODULE
Expand Down Expand Up @@ -324,31 +303,10 @@ public static void ConfigureMemexServices(this WebApplicationBuilder builder)
// manifest and drain when the remote is reachable again).
services.AddInstanceSyncServices();

// Per-user CLI Connect (Settings → Models, CLI providers). The
// ConnectSessionManager is a mesh-scoped singleton holding the live
// login Process between "show URL" and "paste code" (instance dict,
// 5-min timeout). Each gated CLI registers its IConnectStrategy. The
// captured token is persisted as an encrypted ModelProvider via the
// ConnectTokenSink (seam over ModelProviderService, so the AI layer
// never references the portal assembly).
services.AddSingleton<MeshWeaver.AI.Connect.IConnectTokenSink, Memex.Portal.Shared.Models.ConnectTokenSink>();
services.AddSingleton<MeshWeaver.AI.Connect.ConnectSessionManager>();
if (features.Ai.Clis.ClaudeCode)
{
services.AddSingleton<MeshWeaver.AI.Connect.IConnectStrategy, MeshWeaver.AI.Connect.ClaudeConnectStrategy>();
// Wire the Connect login: bind ClaudeConnect:* overrides, default the PTY wrapper ON for
// the co-hosted Linux portal (claude setup-token renders an Ink UI that needs a real TTY —
// see ClaudeConnectStrategy), and mirror the per-user .claude root the co-hosted client uses
// (ClaudeCode:ConfigDirRoot, e.g. /mnt/users) so each user logs in under their own dir.
services.Configure<MeshWeaver.AI.Connect.ClaudeConnectOptions>(o =>
{
builder.Configuration.GetSection("ClaudeConnect").Bind(o);
if (builder.Configuration["ClaudeConnect:UsePseudoTerminal"] is null && !OperatingSystem.IsWindows())
o.UsePseudoTerminal = true;
if (string.IsNullOrEmpty(o.ConfigDirRoot))
o.ConfigDirRoot = builder.Configuration["ClaudeCode:ConfigDirRoot"];
});
}
// Per-user CLI Connect, the model-provider service and the AI content sources are
// registered by the AI MODULE itself (AiMeshModuleAttribute), which reads the same
// configuration this method does. The composition root no longer configures the engine —
// that reference is what a Store-delivered module cannot coexist with (#2276).
// CopilotConnectStrategy registers from the Copilot pack (Modules:Assemblies).

// Social publishing (LinkedIn connect/publish/page-sync + node-menu providers) rides the
Expand Down Expand Up @@ -568,7 +526,7 @@ public static void ValidateContentStorageDurability(
/// <summary>
/// The AI (✨) menu's COMPILED remainder: exactly the imperative "New thread" entry. The
/// navigation entries (Threads / Models / Tiers / Providers / Agents / Skills) migrated to
/// seeded <c>UiContribution</c> nodes (<see cref="AiMenuContributions"/>, WS7 slice 3) — they
/// seeded <c>UiContribution</c> nodes (<c>AiMenuContributions</c>, WS7 slice 3) — they
/// are pure links, so they ride the same lane a plugin's AI-menu entry arrives through.
/// This list stays the SINGLE SOURCE OF TRUTH for the imperative entry, shared by the hub
/// registration below and the unit tests, so a regression can't quietly drop it.
Expand Down Expand Up @@ -807,12 +765,8 @@ public TBuilder ConfigureMemexMesh(IConfiguration configuration, bool isDevelopm
// import — no regression. Default Helm sets ["Doc","Agent","Provider","Harness","Skill"].
var syncPartitions = features.StaticRepoSync.Partitions
.ToHashSet(StringComparer.OrdinalIgnoreCase);
// AI content is served as a UNIT: if the config names ANY AI partition, serve them ALL
// (Agent/Provider/Harness/Skill), so an incomplete list can't leave Skill (or a future AI
// content type) in-memory while the rest go to the DB — and AddAI's per-type serve-from-DB
// gating stays consistent with the static-repo import. See MeshWeaver.AI/AiContentSources.
if (syncPartitions.Overlaps(AiContentSources.ContentPartitions))
syncPartitions.UnionWith(AiContentSources.ContentPartitions);
// NB: the AI partitions are expanded to the whole bundle by the AI module itself
// (AiMeshModuleAttribute.ServeFromPartitions), next to the sources that rule gates.
IReadOnlySet<string> serveFromPartition = syncPartitions;

MeshBuilder mb = builder
Expand Down Expand Up @@ -876,7 +830,6 @@ public TBuilder ConfigureMemexMesh(IConfiguration configuration, bool isDevelopm
// as seeded UiContribution nodes — same lane a plugin's AI-menu entry (or a whole
// TopBar-declared menu) arrives through. Only the imperative "New thread" stays
// compiled (AiMenuItems).
.AddAiMenuContributions()
.AddSpaceType()
// Generic webhook inbox: the WebhookEvent node type behind
// POST /api/hooks/{target} (allowlisted via WebhookInbox:Targets).
Expand All @@ -886,8 +839,7 @@ public TBuilder ConfigureMemexMesh(IConfiguration configuration, bool isDevelopm
// the whole-course navigation (EduCourseNavigationProvider, registered per-hub
// by the type configuration lambdas). The compiled MeshWeaver.Courses types had
// zero instances in any repo or reachable mesh and are deleted.
.AddPortalType()
.AddAI(serveFromPartition);
.AddPortalType();

// The gRPC mesh transport is a MODULE (MeshWeaver.Hosting.Grpc.dll under
// Modules:Assemblies — GrpcMeshModuleAttribute folds AddGrpcHub over this builder:
Expand Down Expand Up @@ -1052,13 +1004,6 @@ public TBuilder ConfigureMemexMesh(IConfiguration configuration, bool isDevelopm
.WithHeartBeatHandler() // silently ack heartbeats on every per-node hub
.AddDefaultLayoutAreas()
// The course-shell areas (StartExercise / GoToMyCopy / CourseNav / Learn)
// ship as in-mesh source in the Edu plugin (Plugins#481) — no platform
// registration remains; only the navigation contributor below is compiled.
.AddThreadsLayoutArea()
// Scope-tabbed AI catalogs (Agents/Skills/Providers/Models) with per-tab
// create buttons — the AI-menu targets below point here. Registered on every
// per-node hub so they resolve when anchored on the type roots (/Agent/AiAgents …).
.AddAiCatalogLayoutAreas()
.AddApiTokensSettingsTab()
// Register your own MeshWeaver installation and get it an instance key.
.AddInstancesSettingsTab()
Expand Down Expand Up @@ -1184,7 +1129,9 @@ public TBuilder ConfigureMemexPortal(IConfiguration configuration)
// MeshWeaver.Graph.
.WithPortalConfiguration(c =>
{
c.TypeRegistry.AddAITypes();
// AI types + the thread area are contributed by the AI module's own
// WithPortalConfiguration (PortalConfigurationRegistry), so this stays
// AI-free (#2276).
return c.AddData().WithGraphTypes();
})
);
Expand Down
23 changes: 3 additions & 20 deletions memex/Memex.Portal.Shared/Settings/PlatformSettingsTabAreas.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,6 @@ public static class PlatformSettingsTabAreas
/// <summary>Layout area rendering <see cref="PublishedSettingsTab.BuildContent"/> (admin-gated).</summary>
public const string PublishedArea = "SettingsPublished";

/// <summary>Layout area rendering <see cref="TokenUsageSettingsTab.BuildContent"/> (admin-gated).</summary>
public const string TokenUsageArea = "SettingsTokenUsage";

/// <summary>
/// Registers the tranches' tab contents as layout areas on the per-node hubs. The settings
Expand All @@ -69,9 +67,7 @@ public static MessageHubConfiguration AddPlatformSettingsTabAreas(this MessageHu
.WithView(UpdatePolicyArea, (host, _) => AdminGated(host,
() => UpdatePolicySettingsTab.BuildContent(host, PaneStack())))
.WithView(PublishedArea, (host, _) => AdminGated(host,
() => PublishedSettingsTab.BuildContent(host, PaneStack())))
.WithView(TokenUsageArea, (host, _) => AdminGated(host,
() => TokenUsageSettingsTab.BuildContent(host, PaneStack()))));
() => PublishedSettingsTab.BuildContent(host, PaneStack()))));

/// <summary>
/// The admin-gated area body. Each admin tab's contributed entry hides the tab via
Expand All @@ -96,12 +92,12 @@ private static IObservable<UiControl> AdminGated(LayoutAreaHost host, Func<UiCon
internal static IReadOnlyList<string> Areas { get; } =
[
WhatsNewArea, AboutArea, PrivacyArea, InvitationsArea, InboxArea,
UpdatePolicyArea, PublishedArea, TokenUsageArea,
UpdatePolicyArea, PublishedArea,
];

/// <summary>
/// The seeded menu entries, exposed for the pinning tests (same contract as
/// <see cref="AiMenuContributions.Seeds"/>). Node id = the tab's former compiled tab id,
/// <c>AiMenuContributions.Seeds</c>). Node id = the tab's former compiled tab id,
/// keeping every <c>/GlobalSettings/{Id}</c> deep link stable across the migration; every
/// label/icon/order/group is copied EXACTLY from the former compiled definition.
/// </summary>
Expand Down Expand Up @@ -180,19 +176,6 @@ private static IObservable<UiControl> AdminGated(LayoutAreaHost host, Func<UiCon
Order = 320,
Gates = new UiContributionGates { AdminOnly = true },
}),
Seed(TokenUsageSettingsTab.TabId, "Token Usage", new UiContribution
{
Context = UiContribution.SettingsContext,
Area = TokenUsageArea,
Label = "Token Usage",
LabelKey = "settings.tokenUsage",
Icon = "Database",
Group = "Administration",
GroupKey = "settings.groupAdministration",
GroupIcon = "Shield",
Order = 320,
Gates = new UiContributionGates { AdminOnly = true },
}),
Seed(PrivacySettingsTab.TabId, "Privacy", new UiContribution
{
Context = UiContribution.SettingsContext,
Expand Down
Loading
Loading