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"]
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",
description: "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.
@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/input or /data/output, preventing directory traversal attacks.
- Pure vs I/O Separation:
extract_metadata declares 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.
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
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-component to generate a WASI P2-ready structure with tool macros and capability declarations.
- Define capabilities: Edit
act.toml to declare exact filesystem paths, network hosts, and cryptographic operations the component requires.
- Implement tools: Write discrete functions using
@actTool or #[act_tool]. Separate pure logic from I/O, and validate all inputs against CBOR schemas.
- Build and attest: Execute
just build && just test to compile to wasm32-wasip2, then run just publish to 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/input to enforce capability boundaries at runtime.