Eingehende Ausloeser: n8n und andere starten Agenten (#725, Stufe 1)
Something outside hands an agent a task - an n8n workflow on a mail, a
schedule, a form, or any system that sends an HTTP request.
- Core/Triggers/InboundTriggers: one token per trigger and agent, kept as a
hash and shown once; a rate per hour; deliveries up to 64 KB. Each
delivery becomes a task whose content is framed as foreign content with its
origin; every delivery, accepted or refused, is in the audit (source
"trigger").
- POST /hooks/trigger on the instance (MCP socket, own token, no browsers);
the manager forwards /hooks/{instance}/trigger. /api/triggers to issue,
list and revoke.
- Agent view: a triggers card with the address and an n8n/curl example.
- Nothing writes back: no right to create or change workflows.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
47a20913b0
commit
73565eff18
@@ -61,6 +61,60 @@ internal static class McpEndpoints
|
||||
: Results.Text(answer, "application/json");
|
||||
});
|
||||
|
||||
// #725: An inbound trigger — n8n or any system that sends a request. Outside /api: its
|
||||
// own token is the key, and it can do one thing — hand its agent a task.
|
||||
app.MapPost(Security.McpTransport.TriggerPath, async (HttpContext context, AppHost host, CancellationToken ct) =>
|
||||
{
|
||||
if (context.Request.Headers.Origin.Count > 0)
|
||||
return Results.StatusCode(StatusCodes.Status403Forbidden);
|
||||
if (host.Triggers is not { } triggers)
|
||||
return ApiEndpoints.Unavailable("No inbound triggers in this instance.");
|
||||
if (context.Request.ContentLength is > Core.Triggers.InboundTriggers.MaxPayloadBytes)
|
||||
return Results.StatusCode(StatusCodes.Status413PayloadTooLarge);
|
||||
|
||||
string payload;
|
||||
using (var reader = new StreamReader(context.Request.Body))
|
||||
{
|
||||
var buffer = new char[Core.Triggers.InboundTriggers.MaxPayloadBytes + 1];
|
||||
var read = await reader.ReadBlockAsync(buffer.AsMemory(), ct);
|
||||
if (read > Core.Triggers.InboundTriggers.MaxPayloadBytes)
|
||||
return Results.StatusCode(StatusCodes.Status413PayloadTooLarge);
|
||||
payload = new string(buffer, 0, read);
|
||||
}
|
||||
|
||||
var result = await triggers.ReceiveAsync(BearerToken(context.Request), payload, ct);
|
||||
return result.Outcome switch
|
||||
{
|
||||
Core.Triggers.TriggerOutcome.Accepted => Results.Json(new { ok = true, taskId = result.TaskId }, statusCode: StatusCodes.Status202Accepted),
|
||||
Core.Triggers.TriggerOutcome.Unauthorized => Results.Json(new { ok = false, error = result.Message }, statusCode: StatusCodes.Status401Unauthorized),
|
||||
Core.Triggers.TriggerOutcome.Limited => Results.Json(new { ok = false, error = result.Message }, statusCode: StatusCodes.Status429TooManyRequests),
|
||||
Core.Triggers.TriggerOutcome.TooLarge => Results.StatusCode(StatusCodes.Status413PayloadTooLarge),
|
||||
_ => Results.Json(new { ok = false, error = result.Message }, statusCode: StatusCodes.Status503ServiceUnavailable)
|
||||
};
|
||||
});
|
||||
|
||||
var triggerAdmin = app.MapGroup("/api/triggers");
|
||||
|
||||
triggerAdmin.MapGet("/", async (AppHost host, CancellationToken ct) =>
|
||||
host.Triggers is { } triggers
|
||||
? Results.Ok((await triggers.ListAsync(ct)).Select(t => new TriggerDto(t.Id, t.Label, t.AgentId, t.PerHour, t.CreatedAtUtc, t.RevokedAtUtc, t.LastUsedAtUtc, t.Received, t.Active)).ToList())
|
||||
: Results.Ok(Array.Empty<TriggerDto>()));
|
||||
|
||||
triggerAdmin.MapPost("/", async (AppHost host, TriggerInputDto input, CancellationToken ct) =>
|
||||
{
|
||||
if (host.Triggers is not { } triggers)
|
||||
return ApiEndpoints.Unavailable("No inbound triggers in this instance.");
|
||||
if (!host.Instance.Agents.Any(a => string.Equals(a.AgentId, input.AgentId, StringComparison.OrdinalIgnoreCase)))
|
||||
return Results.BadRequest(new OutcomeDto(false, $"There is no agent '{input.AgentId}'."));
|
||||
var (t, token) = await triggers.IssueAsync(input.Label ?? "", input.AgentId, input.PerHour ?? 30, ct);
|
||||
return Results.Ok(new TriggerIssuedDto(new TriggerDto(t.Id, t.Label, t.AgentId, t.PerHour, t.CreatedAtUtc, null, null, 0, true), token));
|
||||
});
|
||||
|
||||
triggerAdmin.MapDelete("/{id}", async (AppHost host, string id, CancellationToken ct) =>
|
||||
host.Triggers is { } triggers && await triggers.RevokeAsync(id, ct)
|
||||
? Results.Ok(new OutcomeDto(true, "Revoked."))
|
||||
: Results.NotFound(new OutcomeDto(false, $"No active trigger '{id}'.")));
|
||||
|
||||
// Streamable HTTP lets a server refuse the event stream; this one keeps no sessions.
|
||||
app.MapMethods("/mcp", ["GET", "DELETE"], () => Results.StatusCode(StatusCodes.Status405MethodNotAllowed));
|
||||
|
||||
|
||||
Reference in New Issue
Block a user