Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
769597d
Store media assets with versioned metadata, folders and tags
pournasserian Oct 19, 2026
7232e12
Stop reading an upload one byte past the limit
pournasserian Oct 19, 2026
53368ac
Put media in change sets and add the media tools
pournasserian Oct 19, 2026
ed49ec0
Name only media fields when a field is not one
pournasserian Oct 19, 2026
110a5e4
Merge remote-tracking branch 'origin/v2' into 2542-media-assets
pournasserian Oct 19, 2026
6b95164
Merge branch '2542-media-assets' into 2544-media-change-sets
pournasserian Oct 19, 2026
4f62696
Merge remote-tracking branch 'origin/v2' into 2542-media-assets
pournasserian Oct 19, 2026
3782b3e
Accept only allowed media types whose bytes match them, and validate …
pournasserian Oct 19, 2026
2eff3fd
Merge branch '2542-media-assets' into 2544-media-change-sets
pournasserian Oct 19, 2026
43d4160
Refuse an oversized upload before decoding it
pournasserian Oct 19, 2026
3ceaa12
Refuse control characters in alt text and caption, and match folders …
pournasserian Oct 19, 2026
af8eff8
Merge remote-tracking branch 'origin/v2' into 2542-media-assets
pournasserian Oct 19, 2026
f045838
Merge remote-tracking branch 'origin/2542-media-assets' into 2544-med…
pournasserian Oct 19, 2026
ecdba9b
Merge remote-tracking branch 'origin/v2' into 2544-media-change-sets
pournasserian Oct 19, 2026
f68a3cb
Leave the content model's own change set out of the count
pournasserian Oct 19, 2026
8f96312
Merge remote-tracking branch 'origin/v2' into 2544-media-change-sets
pournasserian Oct 19, 2026
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 CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ Tests run against SQLite in-process. To run the PostgreSQL tests as well, set `F

The browser tests drive the admin in headless Chromium through Playwright: keyboard-only review on the desk and automated WCAG 2.1 AA checks with axe-core. They run when `FLUENTCMS_TEST_BROWSER` is set. Install the browser once after a build with `pwsh tests/FluentCMS.IntegrationTests/bin/Debug/net10.0/playwright.ps1 install chromium`; CI installs it and sets the variable.

To run the host, `dotnet run --project src/FluentCMS.Host` starts the web host on `http://localhost:5000` with a SQLite database in `fluentcms.db`, and `dotnet run --project src/FluentCMS.Host -- mcp` starts the stdio mode. Configuration comes from environment variables such as `Database__ConnectionString`, `Database__Provider` (`sqlite` or `postgresql`, inferred from the connection string when omitted), `JobQueue__WorkerCount`, `Content__DeletedRetention` (how long a deleted entry can be restored, `30.00:00:00` by default), `Entities__VerifyWithin` (how long a fact stays verified before it is listed as stale, `180.00:00:00` by default), `Delivery__PreviewTokenLifetime` (how long a preview token works unless it is revoked or its change set ends, `7.00:00:00` by default), `Media__Storage` (the storage new uploads go to, `local` by default), `Media__LocalDisk__Root` (where the local storage keeps bytes, `media` under the content root by default) and `Media__MaxBytes` (the largest upload, 50 MB by default), and from an optional `appsettings.json` next to the binary.
To run the host, `dotnet run --project src/FluentCMS.Host` starts the web host on `http://localhost:5000` with a SQLite database in `fluentcms.db`, and `dotnet run --project src/FluentCMS.Host -- mcp` starts the stdio mode. Configuration comes from environment variables such as `Database__ConnectionString`, `Database__Provider` (`sqlite` or `postgresql`, inferred from the connection string when omitted), `JobQueue__WorkerCount`, `Content__DeletedRetention` (how long a deleted entry can be restored, `30.00:00:00` by default), `Entities__VerifyWithin` (how long a fact stays verified before it is listed as stale, `180.00:00:00` by default), `Delivery__PreviewTokenLifetime` (how long a preview token works unless it is revoked or its change set ends, `7.00:00:00` by default), `Media__Storage` (the storage new uploads go to, `local` by default), `Media__LocalDisk__Root` (where the local storage keeps bytes, `media` under the content root by default) and `Media__MaxBytes` (the largest upload, 50 MB by default; over MCP on HTTP, the server's 30 MB request limit holds an upload to about 22 MB, since base64 grows a file by a third), and from an optional `appsettings.json` next to the binary.

Agents reach the MCP server (ADR-0007) through either mode. The web host serves streamable HTTP at `/mcp` in stateless mode: each request stands alone, no session id is issued, and a GET answers 405 because the server never opens a stream to the client. `fluentcms mcp` serves the same server over standard input and output to an agent that starts it as a local process, for example with `dotnet run --project src/FluentCMS.Host -- mcp` as its command. It reads the same settings as the web host, so pass it the web host's `Database__ConnectionString` and `DataProtection__KeysDirectory` to work on the same database and key ring. Relative paths resolve against the process's working directory, so give an agent absolute ones. Standard output carries only protocol messages, log lines go to standard error, and the process stops when its standard input closes. The stdio mode runs with the local user's database access, so it is meant for development and single-user setups. Clients on protocol revision 2026-07-28 connect with `server/discover`, earlier ones with `initialize`. Agents read content types, the schema document and change sets with the read-only tools, and propose changes with `open_change_set`, `create_content_type`, `revise_content_type` and `submit_change_set`. A write stores an unpublished version and its operation in the agent's change set together, and the version is not published until a person approves and publishes the set; no tool approves, rejects or publishes. Agents follow review with the read-only desk tools: `get_desk_counts`, `list_desk_queue` and `get_change_set_review`, which see only the change sets the agent opened.

Expand Down
19 changes: 19 additions & 0 deletions src/FluentCMS.Core/Agents/AgentPermissions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,25 @@ public async Task<Result> CheckContentAsync(
+ "credential that includes them.");
}

public async Task<Result> CheckMediaAsync(AgentOperation operation, CancellationToken cancellationToken = default)
{
var agent = await AgentAsync(cancellationToken);
if (!agent.IsSuccess)
{
return Result.Failure(agent.Errors);
}

if (agent.Value is not { } credential || credential.Scope.AllowsOperation(operation))
{
return Result.Success();
}

return Result.Failure(
AgentPermissionErrorCodes.ScopeDenied,
$"The credential '{credential.DisplayName}' may not {Verb(operation)} media: its scope allows "
+ $"{string.Join(", ", credential.Scope.Operations.Select(Verb))} only. Ask an administrator for a credential that allows {Verb(operation)}.");
}

public async Task<Result> CheckDefinitionAsync(string contentType, AgentOperation operation, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(contentType);
Expand Down
3 changes: 3 additions & 0 deletions src/FluentCMS.Core/Agents/IAgentPermissions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ Task<Result> CheckContentAsync(
IReadOnlyCollection<string>? fields = null,
CancellationToken cancellationToken = default);

// Media is not a content type, so a scope allows it by its sites and operations alone, whatever types it lists (MED-03, MCP-05).
Task<Result> CheckMediaAsync(AgentOperation operation, CancellationToken cancellationToken = default);

// A content type's own definition, which covers every field, so a scope that narrows the type to some of its fields is refused.
Task<Result> CheckDefinitionAsync(string contentType, AgentOperation operation, CancellationToken cancellationToken = default);

Expand Down
Loading
Loading