Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ valuable, listed roughly from most to least severe:
- **Snapshot integrity and confidentiality**: reading another actor's
suspend/resume snapshot, or resuming an actor from a tampered snapshot.
- **Routing**: traffic intended for one actor delivered to another.
- **Identity**: forging session identity (for example session JWTs).
- **Identity**: forging actor identity (for example actor JWTs).
- **Control-plane authentication and authorization bypass**: reaching
`ateapi` RPCs without valid credentials (mTLS or JWT auth modes),
misusing workerpool client certificates, or acting beyond what valid
Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,11 @@ agents, but rather a system for running them at scale.

[![Agent Substrate Demo](https://img.youtube.com/vi/ZEzkCFJkzjY/hq1.jpg)](https://www.youtube.com/watch?v=ZEzkCFJkzjY)

*Watch the Agent Substrate cluster multiplex ~250 stateful actor sessions across just 8 physical pods.*
*Watch the Agent Substrate cluster multiplex ~250 stateful actors across just 8 physical pods.*

This demo highlights the core developer experience and "Agentic Infrastructure" capabilities of Substrate:

1. **Instant Session Teleport:** High-performance suspend and resume of actors onto any available worker in the pool with sub-second activation.
1. **Instant Actor Teleport:** High-performance suspend and resume of actors onto any available worker in the pool with sub-second activation.
2. **State Persistence:** Persistent working memory (volatile RAM) and filesystem state preserved perfectly across hibernation cycles via full-state snapshots.
3. **Agent Swarm Multiplexing:** Demonstrates 30x+ oversubscription by "juggling" a large registry of stateful actors onto a small pool of shared physical pods.

Expand All @@ -50,7 +50,7 @@ For more videos and walkthroughs, visit our YouTube channel: **[agent-substrate]

Agent Substrate is designed to be **framework and agent harness agnostic**. Because it manages standard OCI containers at the kernel level (via gVisor), it can host agents built on any stack.

* **Agent Development Kit (ADK):** Native support for ADK-compatible session identity and persistent working memory.
* **Agent Development Kit (ADK):** Native support for ADK-compatible actor identity and persistent working memory.
* **LangChain:** Ideal execution environment for long-running, stateful LangChain agents and sandboxed tool-calling.
* **Claude Code & CodeX:** Support for high-density, stateful coding environments that preserve terminal and filesystem state across sessions.
* **Model Context Protocol (MCP):** Deploy secure, sandboxed MCP servers as Substrate Actors to provide durable tools for any LLM.
Expand Down
156 changes: 81 additions & 75 deletions benchmarking/locust/common/ateapi_pb2.py

Large diffs are not rendered by default.

68 changes: 34 additions & 34 deletions benchmarking/locust/common/ateapi_pb2_grpc.py
Original file line number Diff line number Diff line change
Expand Up @@ -170,14 +170,14 @@ def DeleteActor(self, request, context):
raise NotImplementedError('Method not implemented!')

def ListWorkers(self, request, context):
"""List all workers currently reflected in redis.
"""List Workers.
"""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details('Method not implemented!')
raise NotImplementedError('Method not implemented!')

def ListActors(self, request, context):
"""List all actors currently reflected in redis.
"""List Actors.
"""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details('Method not implemented!')
Expand All @@ -198,7 +198,7 @@ def GetAtespace(self, request, context):
raise NotImplementedError('Method not implemented!')

def ListAtespaces(self, request, context):
"""List all Atespaces.
"""List Atespaces.
"""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details('Method not implemented!')
Expand Down Expand Up @@ -722,13 +722,13 @@ def DebugClear(request,
_registered_method=True)


class SessionIdentityStub:
"""SessionIdentity allows substrate workloads to exchange their
class ActorIdentityStub:
"""ActorIdentity allows substrate workloads to exchange their
infrastructure-level credentials (k8s service account token, etc.) for a
substrate session-level credential. A given substrate session might migrate
substrate actor-level credential. A given substrate actor might migrate
between many different physical workers over the course of its lifecycle,
whereas the session credential's identity will be stable for the life of the
session.
whereas the actor credential's identity will be stable for the life of the
actor.

This service requires authentication. You can authenticate with a Kubernetes
service account token in an `Authorization: Bearer` header, or you can
Expand All @@ -737,8 +737,8 @@ class SessionIdentityStub:
upstream, but we will provide a polyfill based on Pod Certificates).

The broker will check that the service credentials you authenticated with
belong to a Pod that is currently mapped to the requested session in the
session database.
belong to a Pod that is currently mapped to the requested actor in the
actor database.
"""

def __init__(self, channel):
Expand All @@ -748,24 +748,24 @@ def __init__(self, channel):
channel: A grpc.Channel.
"""
self.MintJWT = channel.unary_unary(
'/ateapi.SessionIdentity/MintJWT',
'/ateapi.ActorIdentity/MintJWT',
request_serializer=ateapi__pb2.MintJWTRequest.SerializeToString,
response_deserializer=ateapi__pb2.MintJWTResponse.FromString,
_registered_method=True)
self.MintCert = channel.unary_unary(
'/ateapi.SessionIdentity/MintCert',
'/ateapi.ActorIdentity/MintCert',
request_serializer=ateapi__pb2.MintCertRequest.SerializeToString,
response_deserializer=ateapi__pb2.MintCertResponse.FromString,
_registered_method=True)


class SessionIdentityServicer:
"""SessionIdentity allows substrate workloads to exchange their
class ActorIdentityServicer:
"""ActorIdentity allows substrate workloads to exchange their
infrastructure-level credentials (k8s service account token, etc.) for a
substrate session-level credential. A given substrate session might migrate
substrate actor-level credential. A given substrate actor might migrate
between many different physical workers over the course of its lifecycle,
whereas the session credential's identity will be stable for the life of the
session.
whereas the actor credential's identity will be stable for the life of the
actor.

This service requires authentication. You can authenticate with a Kubernetes
service account token in an `Authorization: Bearer` header, or you can
Expand All @@ -774,30 +774,30 @@ class SessionIdentityServicer:
upstream, but we will provide a polyfill based on Pod Certificates).

The broker will check that the service credentials you authenticated with
belong to a Pod that is currently mapped to the requested session in the
session database.
belong to a Pod that is currently mapped to the requested actor in the
actor database.
"""

def MintJWT(self, request, context):
"""Request a Session Identity JWT.
"""Request an Actor Identity JWT.

To call this RPC, you must be authenticated as the Kubernetes Pod that is
currently running the requested session.
currently running the requested actor.
"""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details('Method not implemented!')
raise NotImplementedError('Method not implemented!')

def MintCert(self, request, context):
"""Request a Session Identity Certificate. To call this RPC, you must have
"""Request an Actor Identity Certificate. To call this RPC, you must have
authenticated with a client certificate, not a bearer token.
"""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details('Method not implemented!')
raise NotImplementedError('Method not implemented!')


def add_SessionIdentityServicer_to_server(servicer, server):
def add_ActorIdentityServicer_to_server(servicer, server):
rpc_method_handlers = {
'MintJWT': grpc.unary_unary_rpc_method_handler(
servicer.MintJWT,
Expand All @@ -811,19 +811,19 @@ def add_SessionIdentityServicer_to_server(servicer, server):
),
}
generic_handler = grpc.method_handlers_generic_handler(
'ateapi.SessionIdentity', rpc_method_handlers)
'ateapi.ActorIdentity', rpc_method_handlers)
server.add_generic_rpc_handlers((generic_handler,))
server.add_registered_method_handlers('ateapi.SessionIdentity', rpc_method_handlers)
server.add_registered_method_handlers('ateapi.ActorIdentity', rpc_method_handlers)


# This class is part of an EXPERIMENTAL API.
class SessionIdentity:
"""SessionIdentity allows substrate workloads to exchange their
class ActorIdentity:
"""ActorIdentity allows substrate workloads to exchange their
infrastructure-level credentials (k8s service account token, etc.) for a
substrate session-level credential. A given substrate session might migrate
substrate actor-level credential. A given substrate actor might migrate
between many different physical workers over the course of its lifecycle,
whereas the session credential's identity will be stable for the life of the
session.
whereas the actor credential's identity will be stable for the life of the
actor.

This service requires authentication. You can authenticate with a Kubernetes
service account token in an `Authorization: Bearer` header, or you can
Expand All @@ -832,8 +832,8 @@ class SessionIdentity:
upstream, but we will provide a polyfill based on Pod Certificates).

The broker will check that the service credentials you authenticated with
belong to a Pod that is currently mapped to the requested session in the
session database.
belong to a Pod that is currently mapped to the requested actor in the
actor database.
"""

@staticmethod
Expand All @@ -850,7 +850,7 @@ def MintJWT(request,
return grpc.experimental.unary_unary(
request,
target,
'/ateapi.SessionIdentity/MintJWT',
'/ateapi.ActorIdentity/MintJWT',
ateapi__pb2.MintJWTRequest.SerializeToString,
ateapi__pb2.MintJWTResponse.FromString,
options,
Expand All @@ -877,7 +877,7 @@ def MintCert(request,
return grpc.experimental.unary_unary(
request,
target,
'/ateapi.SessionIdentity/MintCert',
'/ateapi.ActorIdentity/MintCert',
ateapi__pb2.MintCertRequest.SerializeToString,
ateapi__pb2.MintCertResponse.FromString,
options,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.

package sessionidentity
package actoridentity

import (
"context"
Expand All @@ -28,8 +28,8 @@ import (
"strings"
"time"

"github.com/agent-substrate/substrate/cmd/ateapi/internal/actoridjwt"
"github.com/agent-substrate/substrate/cmd/ateapi/internal/k8sjwt"
"github.com/agent-substrate/substrate/cmd/ateapi/internal/sessionidjwt"
"github.com/agent-substrate/substrate/internal/localca"
"github.com/agent-substrate/substrate/internal/localjwtauthority"
"github.com/agent-substrate/substrate/pkg/proto/ateapipb"
Expand All @@ -40,31 +40,31 @@ import (
"google.golang.org/grpc/status"
)

// Server implements ateapipb.SessionIdentityServer
// Server implements ateapipb.ActorIdentityServer
type Server struct {
ateapipb.UnimplementedSessionIdentityServer
ateapipb.UnimplementedActorIdentityServer

clientJWTIssuer string
clientJWTAudience string

// TODO: Cache the signing keys in memory, so we don't read from a file every time.
sessionIDJWTPoolFile string
sessionIDCAPoolFile string
actorIDJWTPoolFile string
actorIDCAPoolFile string

workerCACerts string
httpClient *http.Client
}

var _ ateapipb.SessionIdentityServer = (*Server)(nil)
var _ ateapipb.ActorIdentityServer = (*Server)(nil)

func New(clientJWTIssuer, clientJWTAudience, sessionIDJWTPoolFile, sessionIDCAPoolFile, workerCACerts string, httpClient *http.Client) *Server {
func New(clientJWTIssuer, clientJWTAudience, actorIDJWTPoolFile, actorIDCAPoolFile, workerCACerts string, httpClient *http.Client) *Server {
return &Server{
clientJWTIssuer: clientJWTIssuer,
clientJWTAudience: clientJWTAudience,
sessionIDJWTPoolFile: sessionIDJWTPoolFile,
sessionIDCAPoolFile: sessionIDCAPoolFile,
workerCACerts: workerCACerts,
httpClient: httpClient,
clientJWTIssuer: clientJWTIssuer,
clientJWTAudience: clientJWTAudience,
actorIDJWTPoolFile: actorIDJWTPoolFile,
actorIDCAPoolFile: actorIDCAPoolFile,
workerCACerts: workerCACerts,
httpClient: httpClient,
}
}

Expand All @@ -91,10 +91,10 @@ func (s *Server) MintJWT(ctx context.Context, req *ateapipb.MintJWTRequest) (*at

// TODO: Extract K8s identity from incoming JWT

// TODO: Cross-check requested session and user claims against the session database.
// TODO: Cross-check requested actor and user claims against the actor database.

// TODO: Cache signing keys in memory, so we don't read from disk every time.
signingPoolBytes, err := os.ReadFile(s.sessionIDJWTPoolFile)
signingPoolBytes, err := os.ReadFile(s.actorIDJWTPoolFile)
if err != nil {
return nil, fmt.Errorf("while reading signing pool bytes: %w", err)
}
Expand All @@ -109,35 +109,35 @@ func (s *Server) MintJWT(ctx context.Context, req *ateapipb.MintJWTRequest) (*at
return nil, fmt.Errorf("at least one audience must be requested")
}

sessionClaims := &sessionidjwt.Claims{
Issuer: "https://broker.agentic-substrate-session-id-broker.svc", // TODO: This needs to be globally unique.
Subject: fmt.Sprintf("apps/%s/users/%s/sessions/%s", req.GetAppId(), req.GetUserId(), req.GetSessionId()),
actorClaims := &actoridjwt.Claims{
Issuer: "https://broker.agentic-substrate-actor-id-broker.svc", // TODO: This needs to be globally unique.
Subject: fmt.Sprintf("apps/%s/users/%s/actors/%s", req.GetAppId(), req.GetUserId(), req.GetActorId()),
Audiences: req.GetAudience(),
Expiration: time.Now().Add(15 * time.Minute),
NotBefore: time.Now().Add(-5 * time.Minute),
IssuedAt: time.Now(),
JTI: rand.Text(),

Substrate: sessionidjwt.SubstrateClaims{
AppID: req.GetAppId(),
UserID: req.GetUserId(),
SessionID: req.GetSessionId(),
Substrate: actoridjwt.SubstrateClaims{
AppID: req.GetAppId(),
UserID: req.GetUserId(),
ActorID: req.GetActorId(),
},
}

sessionWireClaims, err := sessionidjwt.ClaimsToWire(sessionClaims)
actorWireClaims, err := actoridjwt.ClaimsToWire(actorClaims)
if err != nil {
return nil, fmt.Errorf("while making session JWT claims: %w", err)
return nil, fmt.Errorf("while making actor JWT claims: %w", err)
}

// Assume the first authority is the one to use for signing.
sessionJWT, err := sessionidjwt.Sign(sessionWireClaims, signingPool.Authorities[0].SigningKey, signingPool.Authorities[0].Algorithm, signingPool.Authorities[0].ID)
actorJWT, err := actoridjwt.Sign(actorWireClaims, signingPool.Authorities[0].SigningKey, signingPool.Authorities[0].Algorithm, signingPool.Authorities[0].ID)
if err != nil {
return nil, fmt.Errorf("while signing session JWT: %w", err)
return nil, fmt.Errorf("while signing actor JWT: %w", err)
}

return &ateapipb.MintJWTResponse{
SessionJwt: sessionJWT,
ActorJwt: actorJWT,
}, nil
}

Expand All @@ -156,25 +156,25 @@ func (s *Server) MintCert(ctx context.Context, req *ateapipb.MintCertRequest) (*
return nil, status.Errorf(codes.Unauthenticated, "could not verify peer certificate")
}

// TODO: How to verify pod cert <-> session mapping?
// TODO: How to verify pod cert <-> actor mapping?
appID := req.GetAppId()
userID := req.GetUserId()
sessionID := req.GetSessionId()
actorID := req.GetActorId()

if appID == "" || userID == "" || sessionID == "" {
return nil, status.Errorf(codes.InvalidArgument, "app_id, user_id, and session_id are required")
if appID == "" || userID == "" || actorID == "" {
return nil, status.Errorf(codes.InvalidArgument, "app_id, user_id, and actor_id are required")
}

// Load the CA pool for signing
poolBytes, err := os.ReadFile(s.sessionIDCAPoolFile)
poolBytes, err := os.ReadFile(s.actorIDCAPoolFile)
if err != nil {
slog.ErrorContext(ctx, "Failed to read session CA pool file", slog.Any("err", err))
return nil, status.Errorf(codes.Internal, "Failed to load session CA")
slog.ErrorContext(ctx, "Failed to read actor CA pool file", slog.Any("err", err))
return nil, status.Errorf(codes.Internal, "Failed to load actor CA")
}
caPool, err := localca.Unmarshal(poolBytes)
if err != nil || len(caPool.CAs) == 0 {
slog.ErrorContext(ctx, "Failed to load session CA", slog.Any("err", err))
return nil, status.Errorf(codes.Internal, "Failed to load session CA")
slog.ErrorContext(ctx, "Failed to load actor CA", slog.Any("err", err))
return nil, status.Errorf(codes.Internal, "Failed to load actor CA")
}

// Parse the CSR
Expand All @@ -190,8 +190,8 @@ func (s *Server) MintCert(ctx context.Context, req *ateapipb.MintCertRequest) (*

spiffeURI := &url.URL{
Scheme: "spiffe",
Host: "substrate-session.local",
Path: path.Join("app", appID, "user", userID, "session", sessionID),
Host: "substrate-actor.local",
Path: path.Join("app", appID, "user", userID, "actor", actorID),
}
template := &x509.Certificate{
URIs: []*url.URL{spiffeURI},
Expand All @@ -206,7 +206,7 @@ func (s *Server) MintCert(ctx context.Context, req *ateapipb.MintCertRequest) (*
},
}

// Sign and return the session cert.
// Sign and return the actor cert.
ca := caPool.CAs[0]
derBytes, err := x509.CreateCertificate(rand.Reader, template, ca.RootCertificate, csr.PublicKey, ca.SigningKey)
if err != nil {
Expand All @@ -220,6 +220,6 @@ func (s *Server) MintCert(ctx context.Context, req *ateapipb.MintCertRequest) (*
}

return &ateapipb.MintCertResponse{
SessionCertificates: certificates,
ActorCertificates: certificates,
}, nil
}
Loading
Loading