334c19e2 by Ean Schuessler

Implement proper SystemMessage flow for agent responses

Changes:
1. Add contentType='application/json' to SmtyAgentTask SystemMessageType
2. Update run#AgentTask to create response as child SystemMessage
3. Use parentMessageId to link response to original task
4. Use ackMessageId to correlate request/response
5. Update task status to SmsgConfirmed on completion

This follows Moqui framework messaging patterns:
- Queue response message (isOutgoing='Y', status='SmsgProduced')
- Update original with ackMessageId linking to response
- Use proper message flow: Request (SmsgReceived) → Response (SmsgSent) → Confirmed (SmsgConfirmed)
1 parent f88088ae
......@@ -12,6 +12,43 @@ The interface is **model-agnostic** - works with GPT, Claude, local models, or a
---
## 🏗️ Agent Runtime Architecture
Moqui MCP now includes an **Agent Runtime** that allows Moqui to host its own autonomous agents (via OpenAI-compatible APIs like VLLM, Ollama, etc.) that process background tasks.
### Architecture Overview
```
┌────────────────────────┐ ┌────────────────────────┐ ┌────────────────────────┐
│ Moqui Core │ │ Agent Queue │ │ Agent Runtime │
│ │ │ │ │ │
│ User Request │ ---> │ SystemMessage (Pending)│ <--- │ Poll & Lock Message │
│ (Trigger/Service) │ │ Type: AgentTask │ │ │
└────────────────────────┘ └────────────────────────┘ │ 1. Build Prompt │
│ 2. Call VLLM API │
│ 3. Receive Tool Call │
│ 4. Impersonate User │
│ 5. Execute MCP Tool │
│ 6. Save Result │
└────────────────────────┘
```
### Key Components
1. **Agent Client**: Connects to OpenAI-compatible endpoints (VLLM, OpenAI, etc.).
2. **Agent Runner**: Orchestrates the conversation loop (Think → Act → Observe).
3. **Secure Bridge**: Executes tools with user delegation (impersonation) to enforce RBAC.
4. **ProductStoreAiConfig**: Configures AI models and endpoints per Product Store.
### Security Model
- **Authentication**: Agents authenticate as a dedicated service user (e.g., `AGENT_CLAUDE`).
- **Authorization**: Agents **impersonate** the requesting human user for specific tool executions.
- If user `john.doe` cannot create products, the agent acting as `john.doe` cannot create products.
- RBAC is fully enforced at the tool execution layer.
---
## 🧩 How Models Use the Interface
### Discovery Workflow
......
......@@ -190,10 +190,6 @@
// Default temperature if missing
if (temperature == null) temperature = 0.7
// 2. Prepare Tools (Convert MCP tools to OpenAI format)
def mcpToolAdapter = new org.moqui.mcp.adapter.McpToolAdapter()
def moquiTools = mcpToolAdapter.listTools()
// Filter out dangerous tools if needed? For now, we rely on RBAC delegation.
def openAiTools = moquiTools.collect { tool ->
......@@ -284,8 +280,37 @@
} else {
// No tool calls = Final Response
taskComplete = true
taskMsg.statusId = "SmsProcessed"
taskMsg.messageText += "\n\n=== RESPONSE ===\n${responseMsg.content}"
// Create response SystemMessage (child of task)
def responseMsg = responseMsg.content
def responseJson = [
role: "assistant",
content: responseMsg
]
// Queue the response message
ec.service.sync().name("org.moqui.impl.SystemMessageServices.queue#SystemMessage")
.parameters([
systemMessageTypeId: "SmtyAgentTask",
parentMessageId: taskMsg.systemMessageId,
isOutgoing: "Y",
statusId: "SmsgProduced"
messageText: new JsonBuilder(responseJson).toString()
])
.call()
// Update task with ackMessageId
ec.service.sync().name("org.moqui.impl.SystemMessageServices.queue#SystemMessage")
.parameters([
systemMessageId: taskMsg.systemMessageId,
statusId: "SmsgConsumed",
ackMessageId: ec.context.lastResult.systemMessageId
])
.call()
// Update task status and append response to text (for display)
taskMsg.statusId = "SmsgConfirmed"
taskMsg.messageText += "\n\n=== RESPONSE ===\n${responseMsg}"
taskMsg.update()
}
}
......
......@@ -75,7 +75,8 @@ class McpToolAdapter {
logger.debug("Calling tool ${toolName} -> service ${serviceName} with args: ${arguments}")
try {
ec.artifactExecution.disableAuthz()
// NOTE: Authorization is NOT disabled here.
// Tools run with the current user's permissions (or the impersonated user's permissions).
def result = ec.service.sync()
.name(serviceName)
.parameters(arguments ?: [:])
......@@ -89,11 +90,22 @@ class McpToolAdapter {
}
return result ?: [:]
} catch (org.moqui.context.ArtifactAuthorizationException e) {
logger.warn("Security rejection for tool ${toolName} (user: ${ec.user.username}): ${e.message}")
return [
error: [
code: -32001,
message: "Permission Denied: You do not have access to ${e.artifactName}",
data: [
artifact: e.artifactName,
action: e.authzActionEnumId,
message: e.message
]
]
]
} catch (Exception e) {
logger.error("Error calling tool ${toolName}: ${e.message}", e)
return [error: [code: -32000, message: e.message]]
} finally {
ec.artifactExecution.enableAuthz()
}
}
......@@ -114,7 +126,7 @@ class McpToolAdapter {
logger.debug("Calling method ${method} -> service ${serviceName}")
try {
ec.artifactExecution.disableAuthz()
// Standard RBAC applies
def result = ec.service.sync()
.name(serviceName)
.parameters(params ?: [:])
......@@ -128,11 +140,12 @@ class McpToolAdapter {
}
return result ?: [:]
} catch (org.moqui.context.ArtifactAuthorizationException e) {
logger.warn("Security rejection for method ${method}: ${e.message}")
return [error: [code: -32001, message: "Permission Denied: ${e.message}"]]
} catch (Exception e) {
logger.error("Error calling method ${method}: ${e.message}", e)
return [error: [code: -32603, message: "Internal error: ${e.message}"]]
} finally {
ec.artifactExecution.enableAuthz()
}
}
......