Hooks

Overview

Use decorators on your ActingWebApp instance to customize behavior without subclassing:

  • @app.property_hook(name): control access/validation for properties

  • @app.method_hook(name): implement read‑only method endpoints

  • @app.action_hook(name): implement side‑effecting actions

  • @app.lifecycle_hook(event): react to lifecycle events

  • @app.subscription_hook: handle subscription callbacks

Property Hooks

@app.property_hook("email")
def handle_email(actor, operation, value, path):
    if operation == "get":
        return value if actor.is_owner() else None  # hide from others
    if operation in ("put", "post"):
        return value.lower() if "@" in value else None
    return value

Method Hooks

Method hooks implement RPC-style functions. You can add metadata for API discovery:

# Simple method hook
@app.method_hook("get_profile")
def get_profile(actor, method_name, data):
    return {"email": actor.properties.get("email")}

# Method hook with metadata for API discovery
@app.method_hook(
    "calculate",
    description="Perform a mathematical calculation",
    input_schema={
        "type": "object",
        "properties": {"x": {"type": "number"}, "y": {"type": "number"}},
        "required": ["x", "y"]
    },
    annotations={"readOnlyHint": True}
)
def calculate(actor, method_name, data):
    return {"result": data["x"] + data["y"]}

# Method hook with auto-generated schemas from TypedDict
from typing import TypedDict

class ProfileInput(TypedDict):
    include_email: bool

class ProfileOutput(TypedDict):
    name: str
    email: str | None

@app.method_hook("get_profile_detailed", description="Get user profile")
def get_profile_detailed(actor, method_name, data: ProfileInput) -> ProfileOutput:
    return {
        "name": actor.properties.get("name"),
        "email": actor.properties.get("email") if data.get("include_email") else None
    }

Action Hooks

Action hooks implement side-effecting operations. You can add metadata for API discovery:

# Simple action hook
@app.action_hook("send_notification")
def send_notification(actor, action_name, data):
    # send side‑effecting notification
    return {"status": "sent"}

# Action hook with metadata for API discovery
@app.action_hook(
    "delete_item",
    description="Permanently delete an item",
    input_schema={"type": "object", "properties": {"item_id": {"type": "string"}}},
    annotations={"destructiveHint": True}
)
def delete_item(actor, action_name, data):
    delete_from_database(data["item_id"])
    return {"status": "deleted"}

# Action hook with auto-generated schemas from TypedDict
from typing import TypedDict

class CreateNoteInput(TypedDict):
    title: str
    content: str

class CreateNoteOutput(TypedDict):
    note_id: str
    created_at: str

@app.action_hook("create_note", description="Create a new note")
def create_note(actor, action_name, data: CreateNoteInput) -> CreateNoteOutput:
    note_id = save_note(data["title"], data["content"])
    return {"note_id": note_id, "created_at": datetime.now().isoformat()}

Discovering Methods and Actions

Clients can discover available methods and actions via GET requests:

# Discover available methods
curl https://myapp.example.com/<actor_id>/methods

# Discover available actions
curl https://myapp.example.com/<actor_id>/actions

The response includes metadata for each hook:

{
  "methods": [
    {
      "name": "calculate",
      "description": "Perform a mathematical calculation",
      "input_schema": {"type": "object", "properties": {...}},
      "output_schema": null,
      "annotations": {"readOnlyHint": true}
    }
  ]
}

Lifecycle Hooks

@app.lifecycle_hook("actor_created")
def on_actor_created(actor, **kwargs):
    actor.properties.created_at = datetime.now().isoformat()

@app.lifecycle_hook("trust_initiated")
def on_trust_initiated(actor, peer_id, relationship, trust_data, **kwargs):
    # Called when this actor initiates a trust request to a peer
    print(f"Trust initiated to {peer_id}")

@app.lifecycle_hook("trust_request_received")
def on_trust_request_received(actor, peer_id, relationship, trust_data, **kwargs):
    # Called when this actor receives a trust request from a peer
    notify_user(actor, f"New trust request from {peer_id}")

@app.lifecycle_hook("trust_fully_approved_local")
def on_trust_fully_approved_local(actor, peer_id, relationship, trust_data, **kwargs):
    # Called when this actor approves, completing mutual approval
    notify_user(actor, f"You approved! Relationship with {peer_id} established")

@app.lifecycle_hook("trust_fully_approved_remote")
def on_trust_fully_approved_remote(actor, peer_id, relationship, trust_data, **kwargs):
    # Called when peer approves, completing mutual approval
    notify_user(actor, f"{peer_id} approved your request! Relationship established")

@app.lifecycle_hook("subscription_deleted")
def on_subscription_deleted(actor, peer_id, subscription_id, subscription_data, **kwargs):
    # Called when a subscription is deleted (by us or by peer)
    initiated_by_peer = kwargs.get("initiated_by_peer", False)
    if initiated_by_peer:
        # Peer unsubscribed from us - revoke their permissions
        revoke_permissions(actor, peer_id)
        notify_user(actor, f"{peer_id} unsubscribed from your data")

Available events: actor_created, actor_deleted, oauth_success, trust_initiated, trust_request_received, trust_fully_approved_local, trust_fully_approved_remote, trust_deleted, subscription_deleted.

Subscription Hook

@app.subscription_hook
def on_subscription(actor, subscription, peer_id, data):
    if subscription.get("target") == "properties" and "status" in data:
        actor.properties[f"peer_{peer_id}_status"] = data["status"]
    return True

MCP Decorators

Expose hooks via MCP:

  • Tools: decorate an action hook with @mcp_tool

  • Prompts: decorate a method hook with @mcp_prompt

  • Resources: decorate a method hook with @mcp_resource

from actingweb.mcp import mcp_tool, mcp_prompt, mcp_resource

@app.action_hook("create_note")
@mcp_tool(description="Create a note")
def create_note(actor, action_name, params):
    ...

@app.method_hook("analyze_notes")
@mcp_prompt(description="Analyze notes")
def analyze_notes(actor, method_name, params):
    ...

@app.method_hook("config")
@mcp_resource(uri_template="config://{path}")
def config_resource(actor, method_name, params):
    ...

Client-Specific Tool Configuration

The @mcp_tool decorator supports client-specific filtering and descriptions:

@app.action_hook("sensitive_action")
@mcp_tool(
    description="Perform a sensitive action",
    allowed_clients=["claude", "cursor"],  # Only allow Claude and Cursor
    client_descriptions={
        "claude": "Safely perform action with Claude's oversight",
        "cursor": "Execute action within Cursor IDE context"
    }
)
def sensitive_action(actor, action_name, params):
    return {"status": "executed safely"}

Parameters:

  • allowed_clients: List of client types that can access this tool. If None, tool is available to all clients. Supported types: "chatgpt", "claude", "cursor", "mcp_inspector", "universal"

  • client_descriptions: Dict mapping client types to specific descriptions for safety and clarity

Runtime Context

Hooks receive fixed parameters (actor, name, data), but often need context about the current request (which client is calling, session info, etc.). ActingWeb provides a runtime context system to solve this:

from actingweb.runtime_context import RuntimeContext, get_client_info_from_context

@app.action_hook("search")
def handle_search(actor, action_name, data):
    # Get client information for customization
    client_info = get_client_info_from_context(actor)
    if client_info:
        client_type = client_info["type"]  # "mcp", "oauth2", "web"
        client_name = client_info["name"]  # "Claude", "ChatGPT", etc.

        # Customize response based on client
        if client_type == "mcp" and "claude" in client_name.lower():
            # Use Claude-optimized formatting
            return format_for_claude(results)

Context Types

  • MCP Context: Set during MCP client authentication, contains trust relationship with client metadata

  • OAuth2 Context: Set during OAuth2 authentication, contains client ID, user email, scopes

  • Web Context: Set during web requests, contains session ID, user agent, IP address

@app.action_hook("custom_action")
def custom_action(actor, action_name, data):
    runtime_context = RuntimeContext(actor)

    if runtime_context.get_request_type() == "mcp":
        mcp_context = runtime_context.get_mcp_context()
        trust_relationship = mcp_context.trust_relationship
        # Access client_name, client_version, etc.

    elif runtime_context.get_request_type() == "oauth2":
        oauth2_context = runtime_context.get_oauth2_context()
        # Access client_id, user_email, scopes

    elif runtime_context.get_request_type() == "web":
        web_context = runtime_context.get_web_context()
        # Access session_id, user_agent, ip_address

The runtime context is request-scoped and automatically managed by the framework.

Permissions

Permission checks are integrated transparently with hooks. See ActingWeb Access Control (Simple Guide) for the simple guide and ActingWeb Unified Access Control System for full details.