<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Alexander Shishenko</title>
    <description>The latest articles on Forem by Alexander Shishenko (@gamepad64).</description>
    <link>https://forem.com/gamepad64</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F385815%2F10ab8751-e48c-4b19-a16a-d94b15747346.png</url>
      <title>Forem: Alexander Shishenko</title>
      <link>https://forem.com/gamepad64</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/gamepad64"/>
    <language>en</language>
    <item>
      <title>ACT 0.7: sessions, three production bridges, and auth-via-args</title>
      <dc:creator>Alexander Shishenko</dc:creator>
      <pubDate>Sun, 10 May 2026 16:00:00 +0000</pubDate>
      <link>https://forem.com/gamepad64/act-07-sessions-three-production-bridges-and-auth-via-args-278l</link>
      <guid>https://forem.com/gamepad64/act-07-sessions-three-production-bridges-and-auth-via-args-278l</guid>
      <description>&lt;p&gt;The previous posts focused on what ACT &lt;em&gt;is&lt;/em&gt; — sandboxed components, one&lt;br&gt;
binary per transport, capability ceilings. This one is about a thing&lt;br&gt;
that was missing: &lt;strong&gt;state&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Most of the components on &lt;code&gt;actpkg&lt;/code&gt; are pure request/response — &lt;code&gt;crypto&lt;/code&gt;,&lt;br&gt;
&lt;code&gt;encoding&lt;/code&gt;, &lt;code&gt;random&lt;/code&gt;, &lt;code&gt;time&lt;/code&gt;. They don't need a session because every&lt;br&gt;
call is independent. But the moment you reach for tools that &lt;em&gt;do&lt;/em&gt; —&lt;br&gt;
a database connection, an OpenAPI client that has parsed a 5MB spec, an&lt;br&gt;
MCP bridge mid-handshake with an upstream server — there's nowhere to&lt;br&gt;
put that state. Components held it in &lt;code&gt;thread_local!&lt;/code&gt; HashMaps keyed by&lt;br&gt;
&lt;code&gt;std:session-id&lt;/code&gt; metadata, and the host had no idea what those ids&lt;br&gt;
meant or when to clean them up.&lt;/p&gt;

&lt;p&gt;ACT 0.7 fixes that. Stateful components now opt into a small new WIT&lt;br&gt;
interface, &lt;code&gt;act:sessions/session-provider@0.1.0&lt;/code&gt;. The interface is&lt;br&gt;
deliberately tiny — three functions — and it changes nothing for the&lt;br&gt;
80% of components that don't need it.&lt;/p&gt;
&lt;h2&gt;
  
  
  The interface
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;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)
    -&amp;gt; result&amp;lt;string, error&amp;gt;;

  /// Open a session. `args` carries connection params and credentials.
  open-session: async func(args: metadata, metadata: metadata)
    -&amp;gt; result&amp;lt;session, error&amp;gt;;

  /// Polite shutdown — sync, advisory. The host MUST call this for
  /// every session it opened, before component deinit.
  close-session: func(session-id: string);
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Subsequent capability calls (&lt;code&gt;tool-provider.list-tools&lt;/code&gt;,&lt;br&gt;
&lt;code&gt;tool-provider.call-tool&lt;/code&gt;) pass the returned &lt;code&gt;id&lt;/code&gt; as &lt;code&gt;std:session-id&lt;/code&gt;&lt;br&gt;
in their metadata. The component does its own state lookup. The host&lt;br&gt;
keeps the wasm instance alive as long as it's serving sessions, and&lt;br&gt;
closes whatever's still open before it tears the instance down.&lt;/p&gt;

&lt;p&gt;If you used the old &lt;code&gt;metadata = {"url": "..."}&lt;/code&gt; shape on the bridges,&lt;br&gt;
this is a breaking change. The reasoning is in the next section.&lt;/p&gt;
&lt;h2&gt;
  
  
  Three bridges, rebuilt
&lt;/h2&gt;

&lt;p&gt;The whole reason &lt;code&gt;act:sessions&lt;/code&gt; exists is that bridges need it. Three&lt;br&gt;
of them just shipped on the new model:&lt;/p&gt;
&lt;h3&gt;
  
  
  &lt;a href="https://github.com/actpkg/mcp-bridge" rel="noopener noreferrer"&gt;&lt;code&gt;mcp-bridge&lt;/code&gt;&lt;/a&gt; 0.2.0
&lt;/h3&gt;

&lt;p&gt;Wraps a remote MCP server, exposes its tools as ACT tools.&lt;br&gt;
&lt;code&gt;open-session&lt;/code&gt; does the MCP &lt;code&gt;initialize&lt;/code&gt; + &lt;code&gt;notifications/initialized&lt;/code&gt;&lt;br&gt;
handshake against the upstream and stashes the resulting&lt;br&gt;
&lt;code&gt;Mcp-Session-Id&lt;/code&gt; header for the lifetime of the session. The bridge&lt;br&gt;
issues its own outward-facing id and maps the two NAT-style — agents&lt;br&gt;
never see the upstream's id, so swapping upstream-session-ids on&lt;br&gt;
expiry is invisible from the agent's side.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;act run ghcr.io/actpkg/mcp-bridge:0.2.0 &lt;span class="nt"&gt;--mcp&lt;/span&gt;
&lt;span class="c"&gt;# then, agent: open_session({"url": "https://upstream.example/mcp", "auth_token": "sk-..."})&lt;/span&gt;
&lt;span class="c"&gt;#         →  {"id": "mcp_0", "metadata": {}}&lt;/span&gt;
&lt;span class="c"&gt;# then, agent: tools/list with _meta.std:session-id = mcp_0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;a href="https://github.com/actpkg/openapi-bridge" rel="noopener noreferrer"&gt;&lt;code&gt;openapi-bridge&lt;/code&gt;&lt;/a&gt; 0.2.0
&lt;/h3&gt;

&lt;p&gt;Loads an OpenAPI 3.x spec at runtime and exposes each operation as a&lt;br&gt;
local ACT tool. &lt;code&gt;open-session({spec_url, headers})&lt;/code&gt; fetches and parses&lt;br&gt;
the spec eagerly, so connect or parse failures surface at open time&lt;br&gt;
rather than 17 tool calls later. Path/query/header parameters and&lt;br&gt;
JSON request bodies are flattened into a single tool argument schema,&lt;br&gt;
auto-named from &lt;code&gt;operationId&lt;/code&gt; (or synthesised from method + path).&lt;/p&gt;

&lt;p&gt;The parsed spec is cached by &lt;code&gt;spec_url&lt;/code&gt; so multiple sessions targeting&lt;br&gt;
the same API share the parse — opening 10 sessions for the same spec&lt;br&gt;
costs one fetch.&lt;/p&gt;
&lt;h3&gt;
  
  
  &lt;a href="https://github.com/actpkg/act-http-bridge" rel="noopener noreferrer"&gt;&lt;code&gt;act-http-bridge&lt;/code&gt;&lt;/a&gt; 0.2.0
&lt;/h3&gt;

&lt;p&gt;The simplest of the three. Proxies a remote ACT-HTTP host as local ACT&lt;br&gt;
tools. &lt;code&gt;open-session({url, headers})&lt;/code&gt;, then any &lt;code&gt;list-tools&lt;/code&gt; /&lt;br&gt;
&lt;code&gt;call-tool&lt;/code&gt; is forwarded to the upstream component over HTTP. Useful&lt;br&gt;
when you want one local component to delegate to a fleet of remote&lt;br&gt;
ACT components without mounting all of them in your host config.&lt;/p&gt;
&lt;h2&gt;
  
  
  Auth lives in open-session, not metadata
&lt;/h2&gt;

&lt;p&gt;Until 0.7, &lt;code&gt;mcp-bridge&lt;/code&gt; accepted &lt;code&gt;auth_token&lt;/code&gt; as &lt;code&gt;metadata&lt;/code&gt; on every&lt;br&gt;
call. That was wrong on three counts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Auth is per-session, not per-call.&lt;/strong&gt; Once you've authenticated to&lt;br&gt;
the upstream, every subsequent call within that session uses the same&lt;br&gt;
identity. Stuffing the bearer into every &lt;code&gt;tools/call&lt;/code&gt; is duplication&lt;br&gt;
and a bug surface — what if some client puts it in some calls and not&lt;br&gt;
others?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Auth has a schema.&lt;/strong&gt; OAuth wants a token. HTTP basic wants a&lt;br&gt;
username and password. mTLS wants a key and cert. Different per-component.&lt;br&gt;
&lt;code&gt;open-session.args&lt;/code&gt; is component-defined, validated by the host&lt;br&gt;
against the schema returned from &lt;code&gt;get-open-session-args-schema&lt;/code&gt; before&lt;br&gt;
the credentials ever touch the wasm. There's no place in &lt;code&gt;metadata&lt;/code&gt;&lt;br&gt;
to carry that schema.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Auth lives at one boundary.&lt;/strong&gt; With session args, the bearer flows&lt;br&gt;
once: host configuration → host validation → wasm via &lt;code&gt;open-session&lt;/code&gt;.&lt;br&gt;
After that, the agent only ever sees the opaque session-id. If the&lt;br&gt;
session-id leaks, the agent gets capability the operator already&lt;br&gt;
granted. If the bearer leaked from per-call metadata, the agent —&lt;br&gt;
or whoever observed metadata in transit — would have the upstream&lt;br&gt;
credential itself.&lt;/p&gt;

&lt;p&gt;Full guidance is in&lt;br&gt;
&lt;a href="https://github.com/actcore/act-spec/blob/main/spec/ACT-AUTH.md" rel="noopener noreferrer"&gt;&lt;code&gt;ACT-AUTH.md&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  Transport plumbing
&lt;/h2&gt;

&lt;p&gt;The host exposes session lifecycle on the wires it already speaks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;ACT-HTTP&lt;/strong&gt; gains three endpoints (per&lt;br&gt;
&lt;a href="https://github.com/actcore/act-spec/blob/main/spec/ACT-SESSIONS.md#62-act-http" rel="noopener noreferrer"&gt;ACT-SESSIONS §6.2&lt;/a&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;POST  /sessions/open-args-schema   → JSON Schema (metadata in body)
POST  /sessions                    → 201 with session record
DELETE /sessions/{id}              → 204
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Subsequent capability calls reference the session via &lt;code&gt;std:session-id&lt;/code&gt;&lt;br&gt;
in request body metadata or the &lt;code&gt;X-Act-Session-Id&lt;/code&gt; header.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;MCP&lt;/strong&gt; synthesises two virtual tools whenever the underlying&lt;br&gt;
component exports session-provider — &lt;code&gt;open_session&lt;/code&gt; and&lt;br&gt;
&lt;code&gt;close_session&lt;/code&gt;, with &lt;code&gt;_meta.std:session-op&lt;/code&gt; annotations so agents&lt;br&gt;
recognise them as lifecycle ops, not ordinary capabilities. The agent&lt;br&gt;
calls &lt;code&gt;open_session&lt;/code&gt; once, threads &lt;code&gt;_meta.std:session-id&lt;/code&gt; into every&lt;br&gt;
subsequent &lt;code&gt;tools/call&lt;/code&gt;, and finally calls &lt;code&gt;close_session&lt;/code&gt;. The host&lt;br&gt;
also forwards any &lt;code&gt;_meta&lt;/code&gt; keys from the agent into the WIT call&lt;br&gt;
metadata, so &lt;code&gt;std:session-id&lt;/code&gt; reaches the component without any&lt;br&gt;
host-side translation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;ACT-CLI&lt;/strong&gt; picks up a new flag, &lt;code&gt;act call --session-args&lt;/code&gt;. The host&lt;br&gt;
opens a session, threads the returned id into the call's metadata as&lt;br&gt;
&lt;code&gt;std:session-id&lt;/code&gt;, runs the tool, and closes the session — all in one&lt;br&gt;
process, so the wasm instance stays alive for the full sequence.&lt;/p&gt;

&lt;p&gt;A self-contained demo: serve the &lt;code&gt;time&lt;/code&gt; component locally, then&lt;br&gt;
proxy through &lt;code&gt;act-http-bridge&lt;/code&gt; and call it through the proxy.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Terminal 1 — upstream ACT-HTTP server.&lt;/span&gt;
act run ghcr.io/actpkg/time:0.2.0 &lt;span class="nt"&gt;--http&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt; &lt;span class="s1"&gt;'[::1]:3000'&lt;/span&gt;

&lt;span class="c"&gt;# Terminal 2 — one-shot call through the bridge.&lt;/span&gt;
act call ghcr.io/actpkg/act-http-bridge:0.2.0 get_current_time &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--args&lt;/span&gt; &lt;span class="s1"&gt;'{}'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--session-args&lt;/span&gt; &lt;span class="s1"&gt;'{"url":"http://[::1]:3000"}'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--http-policy&lt;/span&gt; open
&lt;span class="c"&gt;# 2026-05-07T12:00:00.000+00:00&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The bridge instance opens a session that owns the upstream URL, calls&lt;br&gt;
&lt;code&gt;get_current_time&lt;/code&gt; through it, closes the session, and exits. With an&lt;br&gt;
&lt;code&gt;openapi-bridge&lt;/code&gt; instead of &lt;code&gt;act-http-bridge&lt;/code&gt; and &lt;code&gt;--session-args&lt;br&gt;
'{"spec_url":"..."}'&lt;/code&gt; instead of &lt;code&gt;{"url":"..."}'&lt;/code&gt;, the same shape&lt;br&gt;
works for any OpenAPI 3.x spec.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;act session open-args-schema&lt;/code&gt; is also still there for inspecting a&lt;br&gt;
component's session args. Earlier 0.7.0 shipped &lt;code&gt;act session open&lt;/code&gt; and&lt;br&gt;
&lt;code&gt;act session close&lt;/code&gt; as separate subcommands, but those were useless:&lt;br&gt;
each invocation is a one-shot process whose wasm instance dies on&lt;br&gt;
exit, so a session opened in one process is unreachable from a later&lt;br&gt;
&lt;code&gt;call&lt;/code&gt;. They were removed in 0.7.1, and &lt;code&gt;--session-args&lt;/code&gt; replaces&lt;br&gt;
both of them with the right shape — the open/call/close cycle in one&lt;br&gt;
process — in 0.7.2.&lt;/p&gt;
&lt;h2&gt;
  
  
  SDK ergonomics
&lt;/h2&gt;

&lt;p&gt;For Rust components, the new&lt;br&gt;
&lt;a href="https://github.com/actcore/act-sdk-rs/blob/main/act-sdk-macros/src/lib.rs" rel="noopener noreferrer"&gt;&lt;code&gt;#[session_open]&lt;/code&gt; / &lt;code&gt;#[session_close]&lt;/code&gt;&lt;/a&gt;&lt;br&gt;
markers on top of &lt;code&gt;act_sdk::SessionRegistry&amp;lt;T&amp;gt;&lt;/code&gt; keep the boilerplate&lt;br&gt;
small:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;act_sdk&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;prelude&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;act_sdk&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;SessionRegistry&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nd"&gt;#[act_component(name&lt;/span&gt; &lt;span class="nd"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"counter"&lt;/span&gt;&lt;span class="nd"&gt;)]&lt;/span&gt;
&lt;span class="k"&gt;mod&lt;/span&gt; &lt;span class="n"&gt;component&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;Counter&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;u64&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nd"&gt;thread_local!&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="n"&gt;SESSIONS&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;SessionRegistry&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Counter&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
            &lt;span class="nn"&gt;SessionRegistry&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"ctr"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nd"&gt;#[derive(Deserialize,&lt;/span&gt; &lt;span class="nd"&gt;JsonSchema)]&lt;/span&gt;
    &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;OpenArgs&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nd"&gt;#[serde(default)]&lt;/span&gt;
        &lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;u64&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nd"&gt;#[session_open]&lt;/span&gt;
    &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;OpenArgs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ActResult&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SESSIONS&lt;/span&gt;&lt;span class="nf"&gt;.with&lt;/span&gt;&lt;span class="p"&gt;(|&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="nf"&gt;.insert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Counter&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="py"&gt;.start&lt;/span&gt; &lt;span class="p"&gt;})))&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nd"&gt;#[session_close]&lt;/span&gt;
    &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;SESSIONS&lt;/span&gt;&lt;span class="nf"&gt;.with&lt;/span&gt;&lt;span class="p"&gt;(|&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="nf"&gt;.remove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// Tools read std:session-id via ActContext&amp;lt;MetaStruct&amp;gt;.&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The macro derives &lt;code&gt;get-open-session-args-schema&lt;/code&gt; from &lt;code&gt;OpenArgs&lt;/code&gt; via&lt;br&gt;
&lt;code&gt;schemars&lt;/code&gt;, decodes the metadata-shaped wire args into the typed&lt;br&gt;
struct, and emits the full session-provider Guest impl. The full&lt;br&gt;
runnable example is&lt;br&gt;
&lt;a href="https://github.com/actcore/act-sdk-rs/tree/main/examples/sessions-counter" rel="noopener noreferrer"&gt;examples/sessions-counter&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Components with dynamic tool catalogs — every bridge — currently&lt;br&gt;
hand-roll &lt;code&gt;wit_bindgen::generate!&lt;/code&gt; because &lt;code&gt;#[act_component]&lt;/code&gt; only&lt;br&gt;
emits a static &lt;code&gt;list-tools&lt;/code&gt; from &lt;code&gt;#[act_tool]&lt;/code&gt; declarations. That's a&lt;br&gt;
known gap; an SDK-side affordance for dynamic catalogs is on the list.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Host-side OAuth.&lt;/strong&gt; ACT-AUTH describes how a host reads
&lt;code&gt;x-act-authorization-server&lt;/code&gt; and &lt;code&gt;x-act-scopes&lt;/code&gt; annotations on the
open-session schema, runs the OAuth flow, and injects the bearer
into args before calling &lt;code&gt;open-session&lt;/code&gt;. The annotations are
spec'd; the host implementation isn't there yet.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Auto-open from host config for &lt;code&gt;--mcp&lt;/code&gt; / &lt;code&gt;--http&lt;/code&gt;.&lt;/strong&gt; &lt;code&gt;act call&lt;/code&gt;
now opens / closes per-invocation via &lt;code&gt;--session-args&lt;/code&gt;, but for the
long-running transports the agent still has to call &lt;code&gt;open_session&lt;/code&gt;
itself. 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.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Postgres component.&lt;/strong&gt; A component that authenticates via
&lt;code&gt;open-session.args&lt;/code&gt;, 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 on &lt;code&gt;actpkg&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;SessionContext&amp;lt;T&amp;gt;&lt;/code&gt; SDK sugar.&lt;/strong&gt; Drop the
&lt;code&gt;ToolMeta { #[serde(rename = "std:session-id")] session_id }&lt;/code&gt;
boilerplate components currently write to read the session-id.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you've got an OpenAPI spec, a remote MCP server, or an internal&lt;br&gt;
ACT-HTTP fleet, the bridges are running on &lt;code&gt;ghcr.io/actpkg&lt;/code&gt; ready for&lt;br&gt;
&lt;code&gt;act run --mcp&lt;/code&gt;. If you want to write a stateful component yourself,&lt;br&gt;
the &lt;a href="https://github.com/actcore/act-sdk-rs/tree/main/examples/sessions-counter" rel="noopener noreferrer"&gt;sessions-counter&lt;br&gt;
example&lt;/a&gt;&lt;br&gt;
is the smallest working template. Issues, ideas, and "this looks like&lt;br&gt;
what I'd want for X but Y is missing" reports are welcome at&lt;br&gt;
&lt;a href="https://github.com/actcore" rel="noopener noreferrer"&gt;github.com/actcore&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>mcp</category>
      <category>webassembly</category>
      <category>opensource</category>
    </item>
    <item>
      <title>MCP servers, sandboxed — introducing ACT</title>
      <dc:creator>Alexander Shishenko</dc:creator>
      <pubDate>Fri, 08 May 2026 17:01:35 +0000</pubDate>
      <link>https://forem.com/gamepad64/mcp-servers-sandboxed-introducing-act-5fa2</link>
      <guid>https://forem.com/gamepad64/mcp-servers-sandboxed-introducing-act-5fa2</guid>
      <description>&lt;p&gt;Setting up an MCP server for your AI agent today usually looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx &lt;span class="nt"&gt;-y&lt;/span&gt; @some-org/mcp-server &lt;span class="c"&gt;# or&lt;/span&gt;
uvx some-mcp-server &lt;span class="c"&gt;# or the occasional&lt;/span&gt;
curl https://example.com/install.sh | bash

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The server runs as you. It can read your home directory. It sees your SSH keys, your &lt;code&gt;.env&lt;/code&gt; files, your shell history, your browser cookies, your GPG keyring. If the server has a bug — or a malicious dependency sneaks in — the code that reads those files also runs as you. If your kernel or any installed binary has an unpatched local privilege escalation, the agent-invoked tool just inherited that escalation path too.&lt;/p&gt;

&lt;p&gt;That isn't a failure mode of any particular MCP server; it's the default deployment model. &lt;strong&gt;Ambient-permission native processes, shipped by anyone, invoked on demand by an LLM that's notoriously easy to talk into misusing them.&lt;/strong&gt;"Your agent has your credentials and runs strangers' code on request" is the baseline security posture of every MCP setup built on &lt;code&gt;npx&lt;/code&gt; / &lt;code&gt;uvx&lt;/code&gt; / &lt;code&gt;curl | bash&lt;/code&gt; today. It's a full-blown security nightmare that the industry has collectively decided not to look at.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;ACT&lt;/strong&gt; — Agent Component Tools — is the model that looks at it.&lt;/p&gt;

&lt;p&gt;Every ACT tool is a WebAssembly component running inside &lt;a href="https://wasmtime.dev" rel="noopener noreferrer"&gt;&lt;code&gt;wasmtime&lt;/code&gt;&lt;/a&gt; — a full VM with a JIT, linear memory, and no ambient host syscalls. Out of the box the component has zero filesystem access, zero outbound network, and no way to spawn a process. Each capability it does use (&lt;code&gt;wasi:filesystem&lt;/code&gt;, &lt;code&gt;wasi:http&lt;/code&gt;) is &lt;strong&gt;declared&lt;/strong&gt; in the component manifest at build time and &lt;strong&gt;granted&lt;/strong&gt; by the operator at run time. The host enforces the intersection: a permissive operator can't escalate past the component's stated intent, a lazy component can't silently exceed the operator's grant. You hand a tool from &lt;code&gt;ghcr.io/someone-else/whatever&lt;/code&gt; to your agent, and the worst-case blast radius is still bounded by the policy you wrote.&lt;/p&gt;

&lt;p&gt;That's the core trade ACT offers. The rest of this post is about why the WebAssembly-component substrate makes it practical.&lt;/p&gt;

&lt;h2&gt;
  
  
  Distribution, for free
&lt;/h2&gt;

&lt;p&gt;A side benefit of picking WebAssembly: the artifact is a single binary that runs everywhere.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;act info ghcr.io/actpkg/random:latest &lt;span class="nt"&gt;--tools&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That command pulls a small component from GitHub's container registry, reads its metadata from a WASM custom section (no instantiation), and prints the tools it exposes. First pull is cached, every subsequent invocation hits the local &lt;code&gt;~/.cache/act/components&lt;/code&gt;. The artifact is signed by GitHub's attestation workflow and comes with an SBOM — all upstream machinery; ACT just uses it.&lt;/p&gt;

&lt;p&gt;Same bytes, same SHA256, on Linux x86_64, macOS arm64, Windows, Android (validated), Raspberry Pi, inside a browser tab, inside a serverless runtime. No per-platform wheels, no native shims, no build toolchain required on anyone's machine but yours. And because the artifact is a registry object rather than three separate npm/pip/cargo packages, there's one supply-chain path to audit instead of three.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;act&lt;/code&gt; accepts components from any of:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;local path: &lt;code&gt;./my-component.wasm&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;HTTP URL: &lt;code&gt;https://example.com/tool.wasm&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;OCI ref: &lt;code&gt;ghcr.io/your-org/tool:1.2.3&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No "my-tool-npm" and "my-tool-pypi" and "my-tool-cargo". One artifact. One namespace.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the sandbox actually works
&lt;/h2&gt;

&lt;p&gt;The isolation comes from three stacked layers, and it's worth separating them because "WASI sandbox" isn't quite the right phrase.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;wasmtime&lt;/strong&gt; is the actual isolation. It's a WebAssembly VM: linear-memory bounds enforced, no direct syscalls, no pointer aliasing, no escape outside of explicit host imports. Every ACT target runs the same wasmtime, so the isolation is identical on every OS and CPU.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;WASI&lt;/strong&gt; is the capability-import layer on top. A component asks for &lt;code&gt;wasi:filesystem&lt;/code&gt; or &lt;code&gt;wasi:http&lt;/code&gt; imports; the host either wires them up, provides a gated proxy, or leaves them unlinked. There's no "deny" at the capability level — a component either has the import or it doesn't.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;ACT&lt;/strong&gt; is the policy layer on top of WASI. Filesystem and outbound network are deny-by-default. The component's manifest declares what it needs (&lt;code&gt;[std.capabilities."wasi:filesystem"]&lt;/code&gt;, &lt;code&gt;"wasi:http"&lt;/code&gt;) — this is a ceiling. The operator's runtime flags (&lt;code&gt;--fs-allow /tmp/db.sqlite&lt;/code&gt;, &lt;code&gt;--http-allow host=api.example.com&lt;/code&gt;) are the grant. The host computes the intersection and refuses to wire up anything outside it.&lt;/p&gt;

&lt;p&gt;Deny-CIDR rules sit in front of DNS resolution, so a component that tries to reach &lt;code&gt;169.254.169.254&lt;/code&gt; (the cloud-metadata service) fails with a &lt;code&gt;DnsError&lt;/code&gt; before a socket opens. HTTP redirects are re-checked per-hop, so a 302 to a denied host fails mid-chain instead of quietly succeeding. Details are in the &lt;a href="https://actcore.dev/blog/" rel="noopener noreferrer"&gt;capability-layer deep-dive&lt;/a&gt; (next post).&lt;/p&gt;

&lt;p&gt;The VM gives us isolation. WASI gives us capability imports. ACT gives us the declaration-plus-ceiling model that makes those capabilities safe to hand to third-party code.&lt;/p&gt;

&lt;h2&gt;
  
  
  One component, any transport
&lt;/h2&gt;

&lt;p&gt;Because tools are components, not native processes, the host can serve them over whatever wire format the caller wants.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Claude Desktop / Cursor / Cline → stdio JSON-RPC&lt;/span&gt;
act run ghcr.io/actpkg/sqlite:latest &lt;span class="nt"&gt;--mcp&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--fs-policy&lt;/span&gt; allowlist &lt;span class="nt"&gt;--fs-allow&lt;/span&gt; /tmp/demo.sqlite

&lt;span class="c"&gt;# Web backend → REST-ish HTTP with SSE streaming&lt;/span&gt;
act run ghcr.io/actpkg/sqlite:latest &lt;span class="nt"&gt;--http&lt;/span&gt; &lt;span class="nt"&gt;--listen&lt;/span&gt; &lt;span class="s2"&gt;"[::1]:3000"&lt;/span&gt;

&lt;span class="c"&gt;# Script / CI → one-shot direct call&lt;/span&gt;
act call ghcr.io/actpkg/sqlite:latest query &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--args&lt;/span&gt; &lt;span class="s1"&gt;'{"sql": "SELECT sqlite_version()"}'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--metadata&lt;/span&gt; &lt;span class="s1"&gt;'{"database_path":"/tmp/demo.sqlite"}'&lt;/span&gt;

&lt;span class="c"&gt;# Browser tab → jco transpile, no server at all&lt;/span&gt;
jco transpile ghcr.io/actpkg/sqlite:latest &lt;span class="nt"&gt;-o&lt;/span&gt; dist/

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same component, same tool, four deployments. Whatever new transport shows up next, the component doesn't change.&lt;/p&gt;

&lt;h2&gt;
  
  
  Writing one
&lt;/h2&gt;

&lt;p&gt;Rust, the whole SDK surface for a trivial tool:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;act_sdk&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;prelude&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nd"&gt;#[act_component]&lt;/span&gt;
&lt;span class="k"&gt;mod&lt;/span&gt; &lt;span class="n"&gt;component&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="nd"&gt;#[act_tool(description&lt;/span&gt; &lt;span class="nd"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Reverse a string"&lt;/span&gt;&lt;span class="nd"&gt;,&lt;/span&gt; &lt;span class="nd"&gt;read_only)]&lt;/span&gt;
    &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;reverse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ActResult&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="nf"&gt;.chars&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.rev&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.collect&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;cargo build --target wasm32-wasip2 --release&lt;/code&gt; + &lt;code&gt;act-build pack&lt;/code&gt; and you have a &lt;code&gt;.wasm&lt;/code&gt; that speaks MCP, HTTP, and the CLI. &lt;code&gt;#[act_tool]&lt;/code&gt; derives the JSON Schema from the function signature; &lt;code&gt;#[act_component]&lt;/code&gt; emits the WIT export. Python has the same shape with &lt;code&gt;@component&lt;/code&gt; / &lt;code&gt;@tool&lt;/code&gt; decorators on top of &lt;a href="https://github.com/bytecodealliance/componentize-py" rel="noopener noreferrer"&gt;&lt;code&gt;componentize-py&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this is
&lt;/h2&gt;

&lt;p&gt;Early, and deliberately narrow.&lt;/p&gt;

&lt;p&gt;The core spec lives in &lt;a href="https://github.com/actcore/act-spec" rel="noopener noreferrer"&gt;actcore/act-spec&lt;/a&gt; — a small WIT package for cross-cutting types and an opt-in interface package for tool dispatch, with stateful capabilities (sessions, events, resources) layered on as separate opt-in packages. The host ships as &lt;code&gt;act&lt;/code&gt; on npm, cargo, and PyPI. A growing set of components is published on &lt;a href="https://github.com/orgs/actpkg/packages" rel="noopener noreferrer"&gt;&lt;code&gt;ghcr.io/actpkg&lt;/code&gt;&lt;/a&gt; — &lt;code&gt;sqlite&lt;/code&gt;, &lt;code&gt;http-client&lt;/code&gt;, &lt;code&gt;crypto&lt;/code&gt;, &lt;code&gt;encoding&lt;/code&gt;, &lt;code&gt;filesystem&lt;/code&gt;, &lt;code&gt;openwallet&lt;/code&gt;, &lt;code&gt;python-eval&lt;/code&gt;, &lt;code&gt;js-eval&lt;/code&gt;, &lt;code&gt;random&lt;/code&gt;, &lt;code&gt;time&lt;/code&gt;, plus three bridges (&lt;code&gt;mcp-bridge&lt;/code&gt;, &lt;code&gt;openapi-bridge&lt;/code&gt;, &lt;code&gt;act-http-bridge&lt;/code&gt;). Rust and Python SDKs are live; JavaScript via &lt;code&gt;componentize-js&lt;/code&gt; is &lt;a href="https://github.com/bytecodealliance/ComponentizeJS/issues/335" rel="noopener noreferrer"&gt;blocked on upstream async-export support&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;More on the architecture:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://actcore.dev/blog/2026-04-24-capability-ceiling/" rel="noopener noreferrer"&gt;The capability ceiling&lt;/a&gt; — declaration-as-ceiling, DNS-level deny-CIDR, per-hop redirect re-check, ancestor traversal, and what goes wrong when any of those is missing.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://actcore.dev/blog/2026-05-07-act-07-sessions/" rel="noopener noreferrer"&gt;Sessions and bridges&lt;/a&gt; — how stateful components (databases, OpenAPI clients, MCP-server proxies) own their per-session state and where authentication actually belongs.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you write MCP servers, build agent tooling, or work on the component model, we'd love your thoughts. Start at &lt;a href="https://actcore.dev/docs/" rel="noopener noreferrer"&gt;actcore.dev/docs&lt;/a&gt;, browse &lt;a href="https://github.com/actcore" rel="noopener noreferrer"&gt;github.com/actcore&lt;/a&gt;, or open a thread in &lt;a href="https://github.com/actcore/act-spec/discussions" rel="noopener noreferrer"&gt;Discussions&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>ai</category>
      <category>opensource</category>
      <category>webassembly</category>
    </item>
  </channel>
</rss>
