picoclaw/docs/magicform-integration.md
admin-mf 3572283d72 Enhance workspace path security and metrics tracking
- 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.
2026-03-06 16:33:50 -06:00

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

  1. Install PicoClaw — see cli.md § Install
  2. 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:

  1. channels.magicform.workspace_root (channel-level override), if set.
  2. agents.defaults.workspace_root (global), if set.
  3. 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 --workspace and --config-dir against agents.defaults.workspace_root using the same validation as the MagicForm webhook. Both entry points use the shared pathutil.ResolveWorkspacePath function.


Troubleshooting

Webhook returns 405 Method Not Allowed

  • The endpoint only accepts POST requests. Ensure you are not sending a GET or other method.

Webhook returns 401 Unauthorized

  • Check that the Authorization: Bearer {token} header matches the token in the MagicForm channel config. Token comparison uses constant-time comparison.

Webhook returns 400 "Invalid workspace" or "Invalid configDir"

  • The workspace or configDir path failed validation. Common causes:
    • Absolute path — use s1/c1, not /data/workspaces/s1/c1.
    • Traversal — paths like ../escape or a/../../etc are rejected.
    • Empty or . — the path must be a subdirectory, not root itself.
    • Escapes root — the resolved path lands outside workspace_root.

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_root nor agents.defaults.workspace_root is configured. Set at least one. The recommended location is agents.defaults.workspace_root.

Callback not received

  • Check that callbackUrl in the payload or backend_url in 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

  • progress and escalation are always present in every callback but set to null when not applicable (e.g. progress is null in a final callback). Check the type field to determine which sub-object to read.

Session not persisting across requests

  • Ensure the same workspace path is sent for the same conversation.
  • Sessions are stored at {workspace}/sessions/. Different workspace paths = different sessions.