A tour of actpkg — components on the protocol
Current Situation Analysis
Agent tooling has historically suffered from a fundamental architectural mismatch: developers build monolithic servers to expose discrete capabilities. Traditional MCP implementations bundle transport, authentication, business logic, and state management into a single process. This creates a fragile attack surface, forces language-specific deployments, and makes capability scoping nearly impossible. When an agent needs to interact with five different APIs, teams typically spin up five separate servers, each with its own dependency tree, credential store, and network footprint.
The ACT protocol addresses this by decoupling tool dispatch from state and transport. The core specification is deliberately minimal: a CBOR-based invocation interface with optional packages for sessions, events, and resources. Instead of shipping full servers, developers publish capability-granted WebAssembly components to OCI registries. Each component declares exactly what it can access (filesystem paths, network hosts, cryptographic operations) and nothing more. The operator enforces these boundaries at runtime, eliminating the need for complex proxy layers or network segmentation.
Despite its elegance, the component model remains underutilized. Most engineering teams default to familiar server patterns because the mental shift from "process isolation" to "capability isolation" requires rethinking how tools are composed, versioned, and secured. Additionally, the ecosystem is still maturing, with language SDKs and bridge adapters rolling out incrementally. The result is a gap between what the protocol enables (deterministic, auditable, cross-language tooling) and what most teams actually deploy (replicated, opaque servers). Bridging this gap requires understanding how to structure components around data-plane, bridge, and pure-function patterns, and how to leverage session-based state management to avoid duplication.
WOW Moment: Key Findings
The architectural shift from monolithic servers to capability-granted components isn't just a packaging change—it fundamentally alters how tools scale, secure, and interoperate. When you compare traditional deployment models against ACT's component architecture, the compounding value of session-based bridges and strict capability boundaries becomes immediately apparent.
| Deployment Model | Sandbox Granularity | Protocol Translation Cost | State Management |
|---|---|---|---|
| Monolithic MCP Server | Process-level (all-or-nothing) | High (custom adapters per API) | In-memory or external DB |
| ACT WASM Component | Capability-level (filesystem, network, crypto) | Low (native bridges via sessions) | Operator-granted, ephemeral or persistent |
This finding matters because it decouples tool functionality from infrastructure overhead. A single bridge component can front hundreds of upstream services by treating each connection as an isolated session. Credentials, base URLs, and retry policies live in session arguments rather than hardcoded configuration or per-call metadata. Meanwhile, pure-function components run with zero declared capabilities, guaranteeing they cannot leak data or make unintended network calls. The result is a toolchain where security boundaries are explicit, deployment artifacts are cryptographically attested, and cross-protocol translation becomes a configuration problem rather than a codebase problem.
Core Solution
Building a production-ready ACT component requires aligning three layers: capability declaration, tool interface design, and session lifecycle management. The following implementation demonstrates a document-indexer component that processes text files, extracts metadata, and stores summaries. It combines pure computation with controlled filesystem access, illustrating how to structure capabilities without over-granting.
Step 1: Define Capability Boundaries
Capabilities must be declared before implementation. The operator uses these declarations to enforce runtime isolation. For this component, we need read access to an input directory and write access to an output directory. Network access is explicitly omitted.
# act.toml
[component]
name = "document-indexer"
version = "0.1.0"
description = "Extracts metadata and generates summaries from text documents"
[capabilities]
filesystem = { read = ["/data/input"], write = ["/data/output"] }
network = []
crypto = ["sha256"]
Step 2: Implement Tool Interfaces
Tools are exposed as discrete functions with strict input/output schemas. We separate pure logic from I/O to maintain testability and security. The extract_metadata tool performs computation only, while store_summary handles filesystem writes.
import { actTool, ToolContext, FsHandle } from "@actcore/sdk";
interface MetadataInput {
content: string;
maxTokens: number;
}
interface MetadataOutput {
wordCount: number;
hash: string;
summary: string;
}
@actTool({
name: "extract_metadata",
description: "Computes word count, SHA-256 hash, and truncated summary",
inputSchema: MetadataInput,
outputSchema: MetadataOutput
})
export async function extractMetadata(ctx: ToolContext, input: MetadataInput): Promise<MetadataOutput> {
const words = input.content.split(/\s+/).filter(Boolean);
const hash = await ctx.crypto.sha256(input.content);
const summary = words.slice(0, input.maxTokens).join(" ");
return {
wordCount: words.length,
hash,
summary
};
}
@actTool({
name: "store_summary",
descrip
tion: "Writes processed metadata to the output directory",
inputSchema: { filename: "string", metadata: MetadataOutput }
})
export async function storeSummary(ctx: ToolContext, input: { filename: string; metadata: MetadataOutput }): Promise<void> {
const outputPath = /data/output/${input.filename}.json;
const handle = await ctx.fs.open(outputPath, { create: true, write: true });
await handle.write(JSON.stringify(input.metadata, null, 2));
await handle.close();
}
### Step 3: Manage Session State
Sessions isolate upstream connections and temporary state. For this component, sessions track processing batches and maintain file handles across multiple tool calls. The operator passes session arguments at invocation time, ensuring credentials and paths never leak into tool logic.
```typescript
@actTool({
name: "init_batch",
description: "Opens a processing session for a directory batch"
})
export async function initBatch(ctx: ToolContext, args: { batchId: string; inputPath: string }): Promise<{ sessionId: string }> {
const session = ctx.sessions.create({
id: args.batchId,
metadata: { inputPath: args.inputPath, processed: 0 }
});
return { sessionId: session.id };
}
Architecture Rationale
- Capability Scoping: Filesystem paths are explicitly allowlisted. The component cannot traverse outside
/data/inputor/data/output, preventing directory traversal attacks. - Pure vs I/O Separation:
extract_metadatadeclares no filesystem or network capabilities. It runs in a hardened sandbox, making it safe for untrusted inputs. - Session-Driven State: Batch tracking lives in session metadata, not global variables. This enables concurrent processing without race conditions.
- OCI Distribution: The component is built to
wasm32-wasip2, pushed to a registry with cryptographic attestation, and verified at runtime. The operator controls execution policy, not the component author.
Pitfall Guide
1. Over-Granting Filesystem Access
Explanation: Declaring broad filesystem permissions (e.g., read: ["/"]) defeats the purpose of capability isolation. Components can read sensitive host files or traverse directories unexpectedly.
Fix: Scope paths to exact directories needed. Use relative paths where possible, and validate input paths against the allowlist before opening handles.
2. Hardcoding Upstream URLs in Bridge Components
Explanation: Embedding API endpoints or credentials in component code breaks reusability and creates security risks. Bridges should remain protocol-agnostic.
Fix: Pass upstream URLs, authentication tokens, and retry policies through open-session arguments. Validate and sanitize these values at session creation time.
3. Ignoring CBOR Schema Validation
Explanation: ACT uses CBOR for wire serialization. Skipping schema validation leads to silent data corruption or type mismatches when agents pass malformed payloads. Fix: Define strict input/output schemas in tool decorators. Use runtime validation libraries that reject unknown fields and enforce type constraints before execution.
4. Mixing Stateful and Stateless Logic in Single Tools
Explanation: Combining computation and I/O in one tool makes testing difficult and violates least-privilege principles. A tool that hashes data shouldn't also write to disk. Fix: Split tools by responsibility. Pure functions handle computation, I/O tools handle storage, and bridge tools handle protocol translation. Compose them at the agent level.
5. Skipping Cryptographic Attestation
Explanation: Distributing components without signed attestations removes supply chain verification. Operators cannot verify that the running binary matches the published artifact.
Fix: Integrate attestation into CI pipelines. Use oras push with signature generation, and configure runtime policies to reject unsigned components.
6. Assuming WASI Network Grants Bypass DNS/Proxy
Explanation: Network capabilities in WASI P2 are not transparent proxies. They enforce host allowlists but do not handle DNS resolution, TLS termination, or proxy routing automatically.
Fix: Declare exact hostnames in network.allow. Use DNS resolution tools if needed, and ensure TLS certificates are valid for the declared hosts.
7. Treating Sessions as Global State
Explanation: Sessions are ephemeral and scoped to a single invocation chain. Storing long-term configuration or user data in sessions causes data loss when sessions expire. Fix: Use sessions for transient state (batch IDs, temporary handles, retry counters). Persist long-term data to filesystem or external databases via dedicated data-plane components.
Production Bundle
Action Checklist
- Scope capabilities to exact paths and hosts before implementation
- Separate pure computation from I/O in tool design
- Pass upstream configuration through session arguments, not code
- Validate all CBOR payloads against strict schemas at runtime
- Generate cryptographic attestations during CI/CD pipeline
- Test components under capability-restricted environments
- Document session lifecycle and expiration policies for operators
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|---|---|---|
| Single API integration with stable endpoint | Data-plane component | Direct filesystem/network access, minimal overhead | Low (one component, one grant) |
| Multiple third-party APIs with varying auth | Bridge component with sessions | One component fronts all upstreams, credentials isolated per session | Medium (session management overhead) |
| Hashing, encoding, or random generation | Pure function component | Zero capabilities, deterministic execution, highly reusable | Negligible (no I/O, no grants) |
| Cross-protocol translation (MCP → ACT) | Reverse adapter bridge | Reuses existing ecosystem without rewriting logic | Low (configuration-only) |
| Long-running stateful workflows | Session + data-plane composition | Sessions track progress, data-plane persists results | Medium (state synchronization) |
Configuration Template
# act.toml
[component]
name = "api-relay"
version = "1.0.0"
description = "Session-based bridge for OpenAPI 3.x services"
[capabilities]
network = { allow = ["api.example.com", "staging.example.com"] }
filesystem = []
crypto = ["sha256", "hmac"]
[session]
ttl_seconds = 3600
max_concurrent = 50
[tools]
invoke_timeout_ms = 5000
retry_attempts = 3
retry_backoff_ms = 200
import { actTool, ToolContext, SessionArgs } from "@actcore/sdk";
interface RelayInput {
method: "GET" | "POST" | "PUT" | "DELETE";
path: string;
headers?: Record<string, string>;
body?: string;
}
@actTool({
name: "forward_request",
description: "Proxies HTTP requests to session-configured upstream"
})
export async function forwardRequest(
ctx: ToolContext,
input: RelayInput
): Promise<{ status: number; body: string }> {
const session = ctx.sessions.current();
const baseUrl = session.args.spec_url as string;
const authHeader = session.args.authorization as string;
const url = new URL(input.path, baseUrl).toString();
const response = await ctx.network.fetch(url, {
method: input.method,
headers: {
...input.headers,
Authorization: authHeader,
"Content-Type": "application/json"
},
body: input.body
});
return {
status: response.status,
body: await response.text()
};
}
Quick Start Guide
- Scaffold the project: Run
copier copy gh:actcore/act-template-rust my-componentto generate a WASI P2-ready structure with tool macros and capability declarations. - Define capabilities: Edit
act.tomlto declare exact filesystem paths, network hosts, and cryptographic operations the component requires. - Implement tools: Write discrete functions using
@actToolor#[act_tool]. Separate pure logic from I/O, and validate all inputs against CBOR schemas. - Build and attest: Execute
just build && just testto compile towasm32-wasip2, then runjust publishto push to your OCI registry with cryptographic signatures. - Run with policy: Invoke via
npx @actcore/act run ghcr.io/your-ns/my-component:latest --fs-policy allowlist --fs-allow /data/inputto enforce capability boundaries at runtime.
