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_toolPrompts: decorate a method hook with
@mcp_promptResources: 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.