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
2 changes: 1 addition & 1 deletion content/ai/Agent/ToolsReference.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ When the user asks "what type is this?", "open the type", or "show me the model"

**Every node you `Create` MUST have an inline SVG `icon`, and every node you `Update`/`Patch` that lacks one should get one. This applies to ALL node types — NodeTypes, data instances, Markdown pages, agents, scripts, everything — not just Markdown.**

- The value must start with `<svg` (inline markup, rendered directly into the page). **Never** a file path (`/static/…​.svg`), **never** an emoji, **never** blank.
- The value must start with `<svg` (inline markup, rendered directly into the page). **Never** a file path (`/api/content/…​.svg`), **never** an emoji, **never** blank.
- **Use `currentColor` for `fill`/`stroke` instead of hard-coded colours.** Inline icons render inheriting the surrounding text colour, so `currentColor` keeps them legible in **both light and dark mode**. A hard-coded dark stroke (e.g. `#0f172a`) disappears on a dark background — that is the #1 icon bug. For multi-colour illustrative icons, pick colours that read on both light and dark surfaces.
- Set an explicit `width`/`height` (e.g. `24`) and a `viewBox` so the icon renders crisply inside the ~48px icon box.
- Make it **distinct and topical** — design the glyph to represent that node's subject (a hurricane swirl for a storm bond, a seismograph trace for an earthquake bond, a shield-and-bolt for an insurance-linked security). Distinct icons make the navigator scannable.
Expand Down
2 changes: 1 addition & 1 deletion content/ai/Skill/space.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ Retrieval mechanism + the visible-vs-embeddable rule are pinned by `LayoutAreaRe
# 6. Give it a logo and an icon

- **`icon`** — a RENDERABLE value shown in lists, menus, and the page header: an inline `<svg>`, an emoji, or an image URL (e.g. `/static/NodeTypeIcons/space.svg`). **NEVER a Fluent icon name** like `Building` (a name is not an image — it won't render). See §2.
- **`logo`** — an image URL or data URI for the large header image (e.g. a served `/static/...svg`). Without a logo the header falls back to the node icon or the name's initials.
- **`logo`** — an image URL or data URI for the large header image. Prefer a `content:logo.svg` reference on the Space, which renders as the access-controlled `/api/content/{Space}/logo.svg`. **Never point it at `/static/…`** for content: that route serves application build assets only and applies no access check, so a Space's logo placed there would be world-readable. Without a logo the header falls back to the node icon or the name's initials.

# 7. (Optional) Link a GitHub repository

Expand Down
2 changes: 1 addition & 1 deletion deploy/aks/DEPLOY-RUNBOOK.md
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,7 @@ az aks command invoke -g memex-aks-rg -n memexaks-cluster --command \
&& kubectl rollout status deploy/memex-portal-deployment -n <env> --timeout=600s"

# c) Verify with REAL signals, looped. HTTP 200 is the Blazor shell and proves nothing.
for i in $(seq 1 10); do curl -s -o /dev/null -w "%{http_code} " https://<host>/static/<space>/content/<file>; done
for i in $(seq 1 10); do curl -s -o /dev/null -w "%{http_code} " https://<host>/api/content/<space>/content/<file>; done
```

### 🧱 NodeType bake — the deploy MUST compile every dynamic NodeType
Expand Down
38 changes: 22 additions & 16 deletions memex/Memex.LocalMesh/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -70,24 +70,30 @@
app.UseMeshWeaverGrpcWeb(); // browser / React-Native gRPC-web (Connect + Deliver)
app.MapMeshWeaverGrpc(); // the mesh gRPC service (Open + Connect + Deliver)

// Serve mesh content collections at /static/{collection}/{filePath} — e.g. the NodeType icons
// (/static/NodeTypeIcons/box.svg) the doc headers <img> reference. The full Blazor portal maps this
// (BlazorHostingExtensions.MapStaticContent); the headless sidecar didn't, so those icons 404'd and the
// React-Native client couldn't render them. AddGraph() already registers the NodeTypeIcons collection
// (embedded resources) on the mesh hub — this just exposes the "known collection" pattern over HTTP.
app.MapGet("/static/{**path}", async (HttpContext ctx, string path) =>
// 🚨 PUBLIC BUILD ASSETS ONLY — /static/{mount}/{file} (issue #587).
//
// The React-Native client's doc headers reference /static/NodeTypeIcons/box.svg; the full Blazor
// portal serves those from the mesh's StaticAssetMounts, and this headless sidecar mirrors it.
// A mount reads its bytes straight out of a shipped assembly's manifest — no content service, no
// hub, no identity. That is the whole contract: this endpoint performs NO access check, so nothing
// access-controlled may be reachable through it.
//
// It used to resolve ANY registered content collection by name, which made every partition's
// uploads world-readable here (the sidecar resolves no identity at all). Mesh content is served by
// the portal's access-controlled /api/content route instead; this sidecar does not serve it.
app.MapGet("/static/{**path}", (HttpContext ctx, string path) =>
{
var slash = path.IndexOf('/');
var mounts = app.Services.GetRequiredService<IMessageHub>().ServiceProvider
.GetServices<StaticAssetMount>();
// Decode FIRST, then validate: `%2E%2E` survives the server's URL normalization and only
// becomes `..` here (StaticAssetMount.Open guards the decoded path).
var decoded = string.Join('/', path.Split('/').Select(Uri.UnescapeDataString));
var slash = decoded.IndexOf('/');
if (slash <= 0) return Results.NotFound();
// Match BlazorHostingExtensions: collection names encode '/' as '~', and file-path segments are
// percent-encoded - decode both so names/files with spaces or UTF-8 resolve instead of false-404ing.
var collection = ContentCollectionsExtensions.DecodeCollectionName(path[..slash]); // "~" -> "/"
var filePath = string.Join('/', path[(slash + 1)..].Split('/').Select(Uri.UnescapeDataString));
var content = app.Services.GetRequiredService<IMessageHub>().ServiceProvider.GetService<IContentService>();
if (content is null) return Results.NotFound();
// HTTP-boundary await: the read leaf runs on the collection's IIoPool; the endpoint awaits
// the replayed single emission only.
var stream = await content.GetContent(collection, filePath).Take(1);
var segment = decoded[..slash];
var filePath = decoded[(slash + 1)..];
var mount = mounts.FirstOrDefault(m => string.Equals(m.Segment, segment, StringComparison.OrdinalIgnoreCase));
var stream = mount?.Open(filePath);
if (stream is null) return Results.NotFound();
var contentType = filePath.EndsWith(".svg", StringComparison.OrdinalIgnoreCase) ? "image/svg+xml"
: filePath.EndsWith(".png", StringComparison.OrdinalIgnoreCase) ? "image/png"
Expand Down
10 changes: 8 additions & 2 deletions memex/Memex.Portal.Monolith/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,15 @@
// Register storage collection at mesh level for static file serving (monolith only)
if (storageConfig != null)
{
// Storage collection: read-only static backing store, hidden from children.
// Storage collection: read-only backing store, hidden from children.
// IsEditable / ExposeInChildren default to false — leave unset.
storageConfig = storageConfig with { IsStatic = true };
//
// 🚨 IsStatic stays FALSE (issue #587). This is the mesh-level store holding EVERY
// partition's content under content/{nodePath}/… . Publishing it by URL is exactly the
// reported hole: /static/storage/content/{node}/{file} handed out any partition's
// uploads at a fully predictable address with no permission check anywhere. The store is
// reached through the per-node collections mapped over it, each owned by a node and
// therefore gated on Read of that node.
config.ConfigureHub(hub => hub.AddContentCollection(_ => storageConfig));
}

Expand Down
2 changes: 1 addition & 1 deletion memex/Memex.Portal.Shared/Api/MeshApiEndpoints.cs
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ public static IEndpointRouteBuilder MapMeshApi(this IEndpointRouteBuilder endpoi
RunString(http, rootHub, ct, ops => ops.Resolve(body.Path)));

// Content-collection directory listing — the read half of the React FileBrowser. Path is
// "{node}/{collection}[/{dir}]"; download uses the existing /content|/static URLs, add uses
// "{node}/{collection}[/{dir}]"; download uses the existing /content|/api/content URLs, add uses
// /upload. Returns { collection, path, editable, items:[…] } (or "Error: …").
group.MapPost("/content/list", (HttpContext http, IMessageHub rootHub, PathBody body, CancellationToken ct) =>
RunString(http, rootHub, ct, ops => ops.ContentList(body.Path)));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,10 @@ public class OnboardingMiddleware(RequestDelegate next, ILogger<OnboardingMiddle
"/_content",
"/_blazor",
"/static/",
// Asset fetches must never be bounced to onboarding — a redirect renders as a broken
// image, not a page. /static covers build assets; /api/content is the access-controlled
// content route mesh images/PDFs/downloads use (issue #587).
"/api/content",
"/favicon.ico",
"/mcp",
"/signin-",
Expand Down
17 changes: 15 additions & 2 deletions memex/Memex.Portal.Shared/MemexConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -838,6 +838,12 @@ public TBuilder ConfigureMemexMesh(IConfiguration configuration, bool isDevelopm
Name = "content",
IsEditable = true,
ExposeInChildren = true,
// isStatic: PUBLISHED on the access-controlled content route — a Space's
// images, thumbnails, PDFs and videos are fetched as
// /api/content/{node}/{file} and /api/content/{Space}/content/{file}.
// Publishing decides REACHABLE, never READABLE: every request is still
// gated on Read of the owning node (issue #587).
IsStatic = true,
BasePath = basePath,
Settings = contentStorageConfig.Settings is { } src
? new Dictionary<string, string>(src) { ["BasePath"] = basePath }
Expand All @@ -847,8 +853,11 @@ public TBuilder ConfigureMemexMesh(IConfiguration configuration, bool isDevelopm
}

// Map "attachments" to "storage" with per-node subdirectory
// (needed by FutuRe and other samples that store datacube.csv, etc.)
config = config.MapContentCollection("attachments", "storage", $"attachments/{nodePath}");
// (needed by FutuRe and other samples that store datacube.csv, etc.).
// isStatic: the file browser's download links are
// /api/content/{node}/attachments/… — access-controlled, gated on Read of the node.
config = config.MapContentCollection(
"attachments", "storage", $"attachments/{nodePath}", isStatic: true);

// Shared large static assets (e.g. the on-device Whisper models the native client
// downloads) live in a FileSystem content collection on the MeshWeaver space, backed
Expand All @@ -866,6 +875,10 @@ public TBuilder ConfigureMemexMesh(IConfiguration configuration, bool isDevelopm
Address = config.Address,
IsEditable = true,
ExposeInChildren = true,
// isStatic: the native client downloads these over
// /api/content/MeshWeaver/static/Speech/… — a real publication, still
// gated on Read of the MeshWeaver node.
IsStatic = true,
Settings = new Dictionary<string, string> { ["BasePath"] = staticAssetsMount },
});

Expand Down
6 changes: 5 additions & 1 deletion memex/aspire/Memex.Portal.Distributed/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -264,7 +264,11 @@
// thing and is always called "storage" (the Monolith's appsettings names it so).
// Registering it under the configured name would both shadow the per-node collection
// and still leave MapContentCollection(…, "storage", …) unresolved.
: hub.AddContentCollection(_ => storageConfig with { Name = "storage", IsStatic = true });
//
// 🚨 IsStatic stays FALSE (issue #587): this mesh-level store holds EVERY partition's
// content, and publishing it by URL was the reported hole. The per-node collections
// mapped over it are what get published — each owned by a node, gated on Read of it.
: hub.AddContentCollection(_ => storageConfig with { Name = "storage" });
});

// Hard gate: refuse to start if the DB isn't migrated. Aspire's
Expand Down
2 changes: 1 addition & 1 deletion samples/Graph/Data/ACME.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"name": "ACME",
"nodeType": "Space",
"description": "Project and task management demo showcasing MeshWeaver's collaborative workflows and AI agent integration",
"icon": "/static/storage/content/ACME/icon.svg",
"icon": "/api/content/ACME/icon.svg",
"addressSegments": 0,
"isPersistent": true,
"content": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ private static UiControl BuildArticleOverview(LayoutAreaHost host, MeshNode node
{
var ns = node.Namespace;
imgSrc = !string.IsNullOrEmpty(ns)
? $"/static/storage/content/{ns}/{thumbnail}"
? $"/api/content/{ns}/{thumbnail}"
: thumbnail;
}
container = container.WithView(Controls.Html(
Expand Down
6 changes: 3 additions & 3 deletions samples/Graph/Data/ACME/Documentation/UnifiedReferences.md
Original file line number Diff line number Diff line change
Expand Up @@ -170,13 +170,13 @@ Defined in `ACME/Project/Todo/Source/Category.cs`:

| Content | Path |
|---------|------|
| Todo icon | `/static/storage/content/ACME/Project/Todo/icon.svg` |
| Project thumbnails | `/static/storage/content/ACME/Project/thumbnails/` |
| Todo icon | `/api/content/ACME/Project/Todo/icon.svg` |
| Project thumbnails | `/api/content/ACME/Project/thumbnails/` |

## Embedding Todo Icon

```markdown
![Todo Icon](/static/storage/content/ACME/Project/Todo/icon.svg)
![Todo Icon](/api/content/ACME/Project/Todo/icon.svg)
```

# Layout Area References
Expand Down
2 changes: 1 addition & 1 deletion samples/Graph/Data/ACME/ProductLaunch.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"name": "TaskFlow Product Launch",
"nodeType": "ACME/Project",
"description": "Launch campaign for TaskFlow - our new workflow automation platform",
"icon": "/static/storage/content/ACME/Project/icon.svg",
"icon": "/api/content/ACME/Project/icon.svg",
"isPersistent": true,
"content": {
"$type": "Project",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"nodeType": "ACME/Project/Todo",
"category": "Tasks",
"description": "Arrange briefings with key industry analysts",
"icon": "/static/storage/content/ACME/Project/Todo/icon.svg",
"icon": "/api/content/ACME/Project/Todo/icon.svg",
"isPersistent": true,
"content": {
"$type": "Todo",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"nodeType": "ACME/Project/Todo",
"category": "Tasks",
"description": "Set up tracking and reporting for launch metrics",
"icon": "/static/storage/content/ACME/Project/Todo/icon.svg",
"icon": "/api/content/ACME/Project/Todo/icon.svg",
"isPersistent": true,
"content": {
"$type": "Todo",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"nodeType": "ACME/Project/Todo",
"category": "Tasks",
"description": "Analyze competitor pricing strategies for similar workflow automation products",
"icon": "/static/storage/content/ACME/Project/Todo/icon.svg",
"icon": "/api/content/ACME/Project/Todo/icon.svg",
"isPersistent": true,
"content": {
"$type": "Todo",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"path": "ACME/ProductLaunch/Todo/BacklogItem2",
"name": "Create ROI calculator tool",
"nodeType": "ACME/Project/Todo",
"icon": "/static/storage/content/ACME/Project/Todo/icon.svg",
"icon": "/api/content/ACME/Project/Todo/icon.svg",
"description": "Build an interactive ROI calculator for prospects to estimate savings",
"category": "Tasks",
"addressSegments": 0,
Expand Down
2 changes: 1 addition & 1 deletion samples/Graph/Data/ACME/ProductLaunch/Todo/CRMSetup.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"nodeType": "ACME/Project/Todo",
"category": "Tasks",
"description": "Set up sales pipeline stages in CRM",
"icon": "/static/storage/content/ACME/Project/Todo/icon.svg",
"icon": "/api/content/ACME/Project/Todo/icon.svg",
"isPersistent": true,
"content": {
"$type": "Todo",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"nodeType": "ACME/Project/Todo",
"category": "Tasks",
"description": "Analyze key competitors in the workflow automation space",
"icon": "/static/storage/content/ACME/Project/Todo/icon.svg",
"icon": "/api/content/ACME/Project/Todo/icon.svg",
"isPersistent": true,
"content": {
"$type": "Todo",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"nodeType": "ACME/Project/Todo",
"description": "Create detailed buyer persona for TaskFlow targeting operations managers at mid-market companies",
"category": "Tasks",
"icon": "/static/storage/content/ACME/Project/Todo/icon.svg",
"icon": "/api/content/ACME/Project/Todo/icon.svg",
"isPersistent": true,
"lastModified": "2026-01-20T10:05:06.1472369+00:00",
"version": 0,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"path": "ACME/ProductLaunch/Todo/DemoEnvironment",
"name": "Set up demo environment",
"nodeType": "ACME/Project/Todo",
"icon": "/static/storage/content/ACME/Project/Todo/icon.svg",
"icon": "/api/content/ACME/Project/Todo/icon.svg",
"description": "Configure a stable demo instance with sample data",
"category": "Tasks",
"addressSegments": 0,
Expand Down
2 changes: 1 addition & 1 deletion samples/Graph/Data/ACME/ProductLaunch/Todo/DemoVideo.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"path": "ACME/ProductLaunch/Todo/DemoVideo",
"name": "Record product demo video",
"nodeType": "ACME/Project/Todo",
"icon": "/static/storage/content/ACME/Project/Todo/icon.svg",
"icon": "/api/content/ACME/Project/Todo/icon.svg",
"description": "Produce a professional product walkthrough video",
"category": "Tasks",
"addressSegments": 0,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"nodeType": "ACME/Project/Todo",
"category": "Tasks",
"description": "Build automated email campaigns for lead nurturing",
"icon": "/static/storage/content/ACME/Project/Todo/icon.svg",
"icon": "/api/content/ACME/Project/Todo/icon.svg",
"isPersistent": true,
"content": {
"$type": "Todo",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"nodeType": "ACME/Project/Todo",
"category": "Tasks",
"description": "Document answers to frequently asked questions",
"icon": "/static/storage/content/ACME/Project/Todo/icon.svg",
"icon": "/api/content/ACME/Project/Todo/icon.svg",
"isPersistent": true,
"content": {
"$type": "Todo",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"path": "ACME/ProductLaunch/Todo/LandingPageDesign",
"name": "Design landing page wireframes",
"nodeType": "ACME/Project/Todo",
"icon": "/static/storage/content/ACME/Project/Todo/icon.svg",
"icon": "/api/content/ACME/Project/Todo/icon.svg",
"description": "Create wireframes for the TaskFlow product landing page",
"category": "Tasks",
"addressSegments": 0,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"nodeType": "ACME/Project/Todo",
"category": "Tasks",
"description": "Organize the official TaskFlow launch webinar",
"icon": "/static/storage/content/ACME/Project/Todo/icon.svg",
"icon": "/api/content/ACME/Project/Todo/icon.svg",
"isPersistent": true,
"content": {
"$type": "Todo",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"nodeType": "ACME/Project/Todo",
"category": "Tasks",
"description": "Ensure all launch materials meet legal requirements",
"icon": "/static/storage/content/ACME/Project/Todo/icon.svg",
"icon": "/api/content/ACME/Project/Todo/icon.svg",
"isPersistent": true,
"content": {
"$type": "Todo",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"nodeType": "ACME/Project/Todo",
"category": "Tasks",
"description": "Develop materials for new customer onboarding",
"icon": "/static/storage/content/ACME/Project/Todo/icon.svg",
"icon": "/api/content/ACME/Project/Todo/icon.svg",
"isPersistent": true,
"content": {
"$type": "Todo",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"nodeType": "ACME/Project/Todo",
"category": "Tasks",
"description": "Recruit integration and channel partners for launch",
"icon": "/static/storage/content/ACME/Project/Todo/icon.svg",
"icon": "/api/content/ACME/Project/Todo/icon.svg",
"isPersistent": true,
"content": {
"$type": "Todo",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"nodeType": "ACME/Project/Todo",
"category": "Tasks",
"description": "Define TaskFlow's unique value proposition and market positioning",
"icon": "/static/storage/content/ACME/Project/Todo/icon.svg",
"icon": "/api/content/ACME/Project/Todo/icon.svg",
"isPersistent": true,
"content": {
"$type": "Todo",
Expand Down
Loading
Loading