The previous posts focused on what ACT is โ sandboxed components, one binary per transport, capability ceilings. This one is about a thing that was missing: state.
Most of the components on actpkg are pure request/response โ crypto,encoding, random, time. They don't need a session because every call is independent. But the moment you reach for tools that do โ a database connection, an OpenAPI client that has parsed a 5MB spec, an MCP bridge mid-handshake with an upstream server โ there's nowhere to put that state. Components held it in thread_local! HashMaps keyed bystd:session-id metadata, and the host had no idea what those ids meant or when to clean them up.
ACT 0.7 fixes that. Stateful components now opt into a small new WIT interface, act:sessions/session-provider@0.1.0. The interface is deliberately tiny โ three functions โ and it changes nothing for the 80% of components that don't need it.
The interface
package act:sessions@0.1.0;
interface session-provider {
use act:core/types@0.4.0.{metadata, error};
record session {
id: string,
metadata: metadata,
}
/// JSON Schema describing valid args for `open-session`.
/// Hosts use this to validate before they invoke the component.
get-open-session-args-schema: async func(metadata: metadata)
-> result<string, error>;
/// Open a session. `args` carries connection params and credentials.
open-session: async func(args: metadata, metadata: metadata)
-> result<session, error>;
/// Polite shutdown โ sync, advisory. The host MUST call this for
/// every session it opened, before component deinit.
close-session: func(session-id: string);
}
Subsequent capability calls (tool-provider.list-tools,tool-provider.call-tool) pass the returned id as std:session-idin their metadata. The component does its own state lookup. The host keeps the wasm instance alive as long as it's serving sessions, and closes whatever's still open before it tears the instance down.
If you used the old metadata = {"url": "..."} shape on the bridges, this is a breaking change. The reasoning is in the next section.
Three bridges, rebuilt
The whole reason act:sessions exists is that bridges need it. Three of them just shipped on the new model:
mcp-bridge 0.2.0
Wraps a remote MCP server, exposes its tools as ACT tools.open-session does the MCP initialize + notifications/initializedhandshake against the upstream and stashes the resultingMcp-Session-Id header for the lifetime of the session. The bridge issues its own outward-facing id and maps the two NAT-style โ agents never see the upstream's id, so swapping upstream-session-ids on expiry is invisible from the agent's side.
act run ghcr.io/actpkg/mcp-bridge:0.2.0 --mcp
# then, agent: open_session({"url": "https://upstream.example/mcp", "auth_token": "sk-..."})
# โ {"id": "mcp_0", "metadata": {}}
# then, agent: tools/list with _meta.std:session-id = mcp_0
openapi-bridge 0.2.0
Loads an OpenAPI 3.x spec at runtime and exposes each operation as a local ACT tool. open-session({spec_url, headers}) fetches and parses the spec eagerly, so connect or parse failures surface at open time rather than 17 tool calls later. Path/query/header parameters and JSON request bodies are flattened into a single tool argument schema, auto-named from operationId (or synthesised from method + path).
The parsed spec is cached by spec_url so multiple sessions targeting the same API share the parse โ opening 10 sessions for the same spec costs one fetch.
act-http-bridge 0.2.0
The simplest of the three. Proxies a remote ACT-HTTP host as local ACT tools. open-session({url, headers}), then any list-tools /call-tool is forwarded to the upstream component over HTTP. Useful when you want one local component to delegate to a fleet of remote ACT components without mounting all of them in your host config.
Auth lives in open-session, not metadata
Until 0.7, mcp-bridge accepted auth_token as metadata on every call. That was wrong on three counts.
Auth is per-session, not per-call. Once you've authenticated to the upstream, every subsequent call within that session uses the same identity. Stuffing the bearer into every tools/call is duplication and a bug surface โ what if some client puts it in some calls and not others?
Auth has a schema. OAuth wants a token. HTTP basic wants a username and password. mTLS wants a key and cert. Different per-component.open-session.args is component-defined, validated by the host against the schema returned from get-open-session-args-schema before the credentials ever touch the wasm. There's no place in metadatato carry that schema.
Auth lives at one boundary. With session args, the bearer flows once: host configuration โ host validation โ wasm via open-session. After that, the agent only ever sees the opaque session-id. If the session-id leaks, the agent gets capability the operator already granted. If the bearer leaked from per-call metadata, the agent โ or whoever observed metadata in transit โ would have the upstream credential itself.
Full guidance is inACT-AUTH.md.
Transport plumbing
The host exposes session lifecycle on the wires it already speaks.
ACT-HTTP gains three endpoints (perACT-SESSIONS ยง6.2):
POST /sessions/open-args-schema โ JSON Schema (metadata in body)
POST /sessions โ 201 with session record
DELETE /sessions/{id} โ 204
Subsequent capability calls reference the session via std:session-idin request body metadata or the X-Act-Session-Id header.
MCP synthesises two virtual tools whenever the underlying component exports session-provider โ open_session andclose_session, with _meta.std:session-op annotations so agents recognise them as lifecycle ops, not ordinary capabilities. The agent calls open_session once, threads _meta.std:session-id into every subsequent tools/call, and finally calls close_session. The host also forwards any _meta keys from the agent into the WIT call metadata, so std:session-id reaches the component without any host-side translation.
ACT-CLI picks up a new flag, act call --session-args. The host opens a session, threads the returned id into the call's metadata asstd:session-id, runs the tool, and closes the session โ all in one process, so the wasm instance stays alive for the full sequence.
A self-contained demo: serve the time component locally, then proxy through act-http-bridge and call it through the proxy.
# Terminal 1 โ upstream ACT-HTTP server.
act run ghcr.io/actpkg/time:0.2.0 --http -l '[::1]:3000'
# Terminal 2 โ one-shot call through the bridge.
act call ghcr.io/actpkg/act-http-bridge:0.2.0 get_current_time \
--args '{}' \
--session-args '{"url":"http://[::1]:3000"}' \
--http-policy open
# 2026-05-07T12:00:00.000+00:00
The bridge instance opens a session that owns the upstream URL, callsget_current_time through it, closes the session, and exits. With anopenapi-bridge instead of act-http-bridge and --session-args '{"spec_url":"..."}' instead of {"url":"..."}', the same shape works for any OpenAPI 3.x spec.
act session open-args-schema is also still there for inspecting a component's session args. Earlier 0.7.0 shipped act session open andact session close as separate subcommands, but those were useless: each invocation is a one-shot process whose wasm instance dies on exit, so a session opened in one process is unreachable from a latercall. They were removed in 0.7.1, and --session-args replaces both of them with the right shape โ the open/call/close cycle in one process โ in 0.7.2.
SDK ergonomics
For Rust components, the new#[session_open] / #[session_close]markers on top of act_sdk::SessionRegistry<T> keep the boilerplate small:
use act_sdk::prelude::*;
use act_sdk::SessionRegistry;
#[act_component(name = "counter")]
mod component {
use super::*;
pub struct Counter { value: u64 }
thread_local! {
static SESSIONS: SessionRegistry<Counter> =
SessionRegistry::new("ctr");
}
#[derive(Deserialize, JsonSchema)]
struct OpenArgs {
#[serde(default)]
start: u64,
}
#[session_open]
fn open(args: OpenArgs) -> ActResult<String> {
Ok(SESSIONS.with(|r| r.insert(Counter { value: args.start })))
}
#[session_close]
fn close(id: String) {
SESSIONS.with(|r| { r.remove(&id); });
}
// Tools read std:session-id via ActContext<MetaStruct>.
}
The macro derives get-open-session-args-schema from OpenArgs viaschemars, decodes the metadata-shaped wire args into the typed struct, and emits the full session-provider Guest impl. The full runnable example isexamples/sessions-counter.
Components with dynamic tool catalogs โ every bridge โ currently hand-roll wit_bindgen::generate! because #[act_component] only emits a static list-tools from #[act_tool] declarations. That's a known gap; an SDK-side affordance for dynamic catalogs is on the list.
What's next
-
Host-side OAuth. ACT-AUTH describes how a host reads
x-act-authorization-serverandx-act-scopesannotations on the open-session schema, runs the OAuth flow, and injects the bearer into args before callingopen-session. The annotations are spec'd; the host implementation isn't there yet. -
Auto-open from host config for
--mcp/--http.act callnow opens / closes per-invocation via--session-args, but for the long-running transports the agent still has to callopen_sessionitself. For "I always want this OpenAPI / MCP server / API" configurations, the host should pre-open the session at startup from config, so the agent sees ordinary tools and never thinks about the session at all. -
Postgres component. A component that authenticates via
open-session.args, holds a real connection through the session, and does parameterised queries through the tools. Not ready yet โ this'll be the first non-bridge stateful component onactpkg. -
SessionContext<T>SDK sugar. Drop theToolMeta { #[serde(rename = "std:session-id")] session_id }boilerplate components currently write to read the session-id.
If you've got an OpenAPI spec, a remote MCP server, or an internal ACT-HTTP fleet, the bridges are running on ghcr.io/actpkg ready foract run --mcp. If you want to write a stateful component yourself, the sessions-counter exampleis the smallest working template. Issues, ideas, and "this looks like what I'd want for X but Y is missing" reports are welcome atgithub.com/actcore.

