- Introduced `workspace_root` configuration in global and MagicForm settings to enforce path security. - Updated CLI commands to require relative paths for `--workspace` and `--config-dir` when `workspace_root` is set. - Added detailed validation rules for workspace paths to prevent directory traversal and absolute paths. - Enhanced agent processing to track execution metrics, including token usage and tool calls, and included these metrics in outbound messages. - Updated callback payloads for MagicForm to support progress and escalation messages, including detailed metrics for final responses. - Refactored agent loop to accumulate metrics across LLM iterations and publish progress updates during tool execution. - Added tests to ensure proper handling of new metrics and workspace path validations.
14 KiB
MagicForm Integration Spec
MagicForm delegates agentic tasks to PicoClaw via webhooks. PicoClaw processes the request asynchronously and POSTs the result back to a callback URL.
For general PicoClaw installation, CLI usage, and config reference, see cli.md.
Pre-requisites
- Install PicoClaw — see cli.md § Install
- Global config — see cli.md § Global Config
Directory layout
MagicForm pre-provisions directories on disk before calling PicoClaw (see cli.md § Directory Layout for the general structure):
{workspace_root}/
{stackId}/
config/ # configDir -- shared per-stack
config.json # API key, model, agent settings for this stack
AGENTS.md # Agent instructions (optional)
IDENTITY.md # Agent identity (optional)
SOUL.md # Agent personality (optional)
USER.md # User context (optional)
{conversationId}/ # workspace -- per-conversation
sessions/ # Conversation history (managed by PicoClaw)
memory/ # Persistent agent memory (managed by PicoClaw)
skills/ # Workspace-local skills (optional)
Workspace config
Per-stack config overlays are placed in the config directory. See cli.md § Workspace Config for merge rules and example.
Gateway Mode
Channel config
Add to ~/.picoclaw/config.json:
{
"agents": {
"defaults": {
"workspace_root": "/data/workspaces"
}
},
"gateway": {
"host": "0.0.0.0",
"port": 18790
},
"channels": {
"magicform": {
"enabled": true,
"token": "your-shared-secret",
"backend_url": "https://api.magicform.example.com",
"webhook_path": "/hooks/magicform",
"allow_from": []
}
}
}
| Field | Description | Default |
|---|---|---|
token |
Bearer token for webhook auth. Empty = allow all (dev only). | "" |
backend_url |
Fallback callback URL base (used when payload omits callbackUrl). |
"" |
webhook_path |
HTTP path for the webhook endpoint. | /hooks/magicform |
workspace_root |
Channel-level override for workspace path validation root. If not set, falls back to agents.defaults.workspace_root. At least one must be configured. |
"" |
allow_from |
Sender ID allowlist. Empty = allow all. Accepts strings and numbers (e.g. ["user1", 12345]). |
[] |
workspace_root resolution: The MagicForm channel determines its effective workspace root using:
channels.magicform.workspace_root(channel-level override), if set.agents.defaults.workspace_root(global), if set.- If neither is configured, the gateway fails to start with an error. A workspace root is required for MagicForm because all webhook paths must be validated against a boundary.
The recommended approach is to set workspace_root once in agents.defaults so that it applies to both the CLI and all gateway channels. Use the channel-level workspace_root only if MagicForm needs a different root than other entry points.
All fields can be set via environment variables:
PICOCLAW_CHANNELS_MAGICFORM_ENABLED=true
PICOCLAW_CHANNELS_MAGICFORM_TOKEN=your-shared-secret
PICOCLAW_CHANNELS_MAGICFORM_BACKEND_URL=https://api.magicform.example.com
PICOCLAW_CHANNELS_MAGICFORM_WEBHOOK_PATH=/hooks/magicform
PICOCLAW_CHANNELS_MAGICFORM_WORKSPACE_ROOT=/data/workspaces
PICOCLAW_CHANNELS_MAGICFORM_ALLOW_FROM=sender1,sender2
PICOCLAW_AGENTS_DEFAULTS_WORKSPACE_ROOT=/data/workspaces
Start the gateway
picoclaw gateway
# or with debug logging:
picoclaw gateway -d
Listens on {host}:{port} (default 127.0.0.1:18790).
Health check
GET /health/magicform
Response:
{"status": "ok", "channel": "magicform"}
Webhook: send a message
POST /hooks/magicform
Authorization: Bearer your-shared-secret
Content-Type: application/json
Request body
{
"stackId": "s1",
"conversationId": "c1",
"userId": "user-123",
"message": "Summarize the latest sales report",
"workspace": "s1/c1",
"configDir": "s1/config",
"callbackUrl": "https://api.magicform.example.com/claw-agent/callback",
"allowedTools": ["read_file", "web_fetch"],
"allowedSkills": ["summarize"]
}
| Field | Type | Required | Description |
|---|---|---|---|
stackId |
string | Yes | Tenant/stack identifier. |
conversationId |
string | Yes | Conversation identifier. |
userId |
string | No | Sender identifier (defaults to "anonymous"). |
message |
string | Yes | The user's message. |
workspace |
string | No | Agent working directory, relative to workspace_root. |
configDir |
string | No | Config directory, relative to workspace_root. Contains config.json and bootstrap files. |
callbackUrl |
string | No | Where to POST the response. Falls back to backend_url + "/claw-agent/callback". |
allowedTools |
string[] | No | Tool allowlist. Empty = all tools enabled. See cli.md § Tool Names. |
allowedSkills |
string[] | No | Skill filter. Empty = all skills loaded. |
Path security: workspace and configDir must be relative paths that resolve to subdirectories under workspace_root. The following are rejected with 400 Bad Request:
- Absolute paths (e.g.
/data/workspaces/s1/c1) - Directory traversal (
../escape,a/../../etc,foo/..) - Empty string or bare
.(would resolve to root itself)
See cli.md § Path Security for the full validation rules shared across CLI and gateway.
Request size limit: Webhook payloads are limited to 1 MB. Larger requests receive 413 Request Entity Too Large.
Method: Only POST is accepted. Other methods receive 405 Method Not Allowed.
Response
Returns 200 OK immediately. Processing happens asynchronously.
Callback: receive the result
PicoClaw POSTs results back to the callback URL. There are three callback types: final, progress, and escalation.
POST {callbackUrl}
Authorization: Bearer your-shared-secret
Content-Type: application/json
Common fields
Every callback includes these fields:
| Field | Type | Description |
|---|---|---|
stackId |
string | Echoed from request. |
conversationId |
string | Echoed from request. |
taskId |
string | Unique task ID: claw_task_{conversationId}_{createdAtMs}. |
type |
string | "final", "progress", or "escalation". |
status |
string | "success" or "error". |
response |
string | The agent's response text (may be empty for progress/escalation). |
runtime |
string | Always "picoclaw". |
Final callback
Sent once when the agent finishes processing. Includes execution metrics.
{
"stackId": "s1",
"conversationId": "c1",
"taskId": "claw_task_c1_1709712000000",
"type": "final",
"status": "success",
"response": "Here is the summary of the latest sales report...",
"runtime": "picoclaw",
"durationMs": 4523,
"tokenUsage": {
"promptTokens": 1200,
"completionTokens": 350,
"totalTokens": 1550,
"model": "anthropic/claude-sonnet-4.6"
},
"toolCalls": 3,
"progress": null,
"escalation": null
}
| Field | Type | Description |
|---|---|---|
durationMs |
number | Total processing time in milliseconds. |
tokenUsage |
object | null | Token consumption breakdown. |
tokenUsage.promptTokens |
number | Prompt tokens used across all iterations. |
tokenUsage.completionTokens |
number | Completion tokens used across all iterations. |
tokenUsage.totalTokens |
number | Total tokens (prompt + completion). |
tokenUsage.model |
string | Model ID used for generation. |
toolCalls |
number | Total tool calls made during processing. |
error |
string | Error message (present only when status is "error"). |
progress |
null | Always null for final callbacks. |
escalation |
null | Always null for final callbacks. |
Progress callback
Sent during processing as the agent executes tools. Multiple progress callbacks may be sent before the final callback.
{
"stackId": "s1",
"conversationId": "c1",
"taskId": "claw_task_c1_1709712000000",
"type": "progress",
"status": "success",
"response": "Running tools: web_fetch",
"runtime": "picoclaw",
"progress": {
"status": "thinking",
"toolName": "web_fetch",
"stepNumber": 2,
"message": "Running tools: web_fetch"
},
"escalation": null
}
| Field | Type | Description |
|---|---|---|
progress |
object | Progress details. |
progress.status |
string | Current status (e.g. "thinking"). |
progress.toolName |
string | Name of the tool being executed. |
progress.stepNumber |
number | Current iteration number. |
progress.message |
string | Human-readable progress description. |
Escalation callback
Sent when the agent determines it needs human intervention.
{
"stackId": "s1",
"conversationId": "c1",
"taskId": "claw_task_c1_1709712000000",
"type": "escalation",
"status": "success",
"response": "",
"runtime": "picoclaw",
"progress": null,
"escalation": {
"reason": "User requested human support",
"notes": "Customer issue requires billing system access"
}
}
| Field | Type | Description |
|---|---|---|
escalation |
object | Escalation details. |
escalation.reason |
string | Why escalation is needed. |
escalation.notes |
string | Additional context (optional). |
Session isolation
Each request gets a unique session key: agent:main:magicform:{stackId}:{conversationId}.
Sessions are stored at {workspace}/sessions/. Different conversations within the same stack share the config directory (API keys, bootstrap files) but have separate workspace directories, sessions, and memory.
Processing flow
MagicForm PicoClaw Gateway
| |
|-- POST /hooks/magicform ----------->|
| validate token
| parse payload
| ResolveWorkspacePath(root, workspace) ← 400 on failure
| ResolveWorkspacePath(root, configDir) ← 400 on failure
|<------------ 200 OK ---------------|
| |
| publish InboundMessage to bus
| |
| Agent loop:
| re-validate workspace/configDir (defense-in-depth)
| create temp sessions + context
| copyBootstrapFiles(configDir -> workspace)
| loadWorkspaceConfig(configDir)
| mergeWorkspaceConfig into cloned global cfg
| createProvider (per-request)
| apply tool/skill filters
| run LLM iterations:
| for each iteration:
| LLM call → accumulate token usage
| tool calls → accumulate tool count
| |
|<-- POST callbackUrl (progress) ----| ← during tool execution
| |
| ... more iterations ...
| |
|<-- POST callbackUrl (final) -------| ← with metrics (duration, tokens, tool calls)
Testing with CLI
You can test the same workspace/config setup without the gateway using picoclaw agent. See cli.md § picoclaw agent for full flag reference.
# One-shot with tenant isolation (same relative paths MagicForm would use)
picoclaw agent -m "Summarize the report" \
-s s1:c1 \
--workspace s1/c1 \
--config-dir s1/config
# Restricted tools, matching a webhook allowedTools filter
picoclaw agent -m "Search the web for recent news" \
--tools web,web_fetch
# Debug mode to see session key, model, and iteration details
picoclaw agent -d -m "Hello" -s test
Note
: The CLI resolves
--workspaceand--config-diragainstagents.defaults.workspace_rootusing the same validation as the MagicForm webhook. Both entry points use the sharedpathutil.ResolveWorkspacePathfunction.
Troubleshooting
Webhook returns 405 Method Not Allowed
- The endpoint only accepts
POSTrequests. Ensure you are not sending aGETor other method.
Webhook returns 401 Unauthorized
- Check that the
Authorization: Bearer {token}header matches thetokenin the MagicForm channel config. Token comparison uses constant-time comparison.
Webhook returns 400 "Invalid workspace" or "Invalid configDir"
- The
workspaceorconfigDirpath failed validation. Common causes:- Absolute path — use
s1/c1, not/data/workspaces/s1/c1. - Traversal — paths like
../escapeora/../../etcare rejected. - Empty or
.— the path must be a subdirectory, not root itself. - Escapes root — the resolved path lands outside
workspace_root.
- Absolute path — use
Webhook returns 413 Request Entity Too Large
- The request payload exceeds the 1 MB limit. Reduce the message size.
Gateway fails to start: "magicform channel requires workspace_root"
- Neither
channels.magicform.workspace_rootnoragents.defaults.workspace_rootis configured. Set at least one. The recommended location isagents.defaults.workspace_root.
Callback not received
- Check that
callbackUrlin the payload orbackend_urlin config is reachable from PicoClaw. - Check PicoClaw logs for callback errors.
- Request contexts expire after 10 minutes.
- Progress and escalation callbacks keep the request context alive. The final callback deletes it.
Unexpected null in progress/escalation fields
progressandescalationare always present in every callback but set tonullwhen not applicable (e.g.progressisnullin a final callback). Check thetypefield to determine which sub-object to read.
Session not persisting across requests
- Ensure the same
workspacepath is sent for the same conversation. - Sessions are stored at
{workspace}/sessions/. Different workspace paths = different sessions.