From 535d153a1e2848648350fc79ce85bb5dbce5c972 Mon Sep 17 00:00:00 2001
From: luke-lombardi <33990301+luke-lombardi@users.noreply.github.com>
Date: Thu, 2 Jan 2025 16:20:14 -0500
Subject: [PATCH] Feat: beam shell (#817)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Adds a shell command to let users connect to containers with their stub
config. It can be used with any decorator that inherits from the
`DeployableMixin`.
---
bin/gen_proto.sh | 3 +
docker/Dockerfile.runner | 2 +-
pkg/abstractions/shell/http.go | 107 +++++
pkg/abstractions/shell/shell.go | 365 ++++++++++++++++++
pkg/abstractions/shell/shell.proto | 18 +
pkg/api/v1/stub.go | 2 +-
pkg/common/key_events.go | 4 +
pkg/common/url.go | 2 +-
pkg/common/url_test.go | 11 +-
pkg/gateway/gateway.go | 18 +
pkg/gateway/services/stub.go | 6 +-
.../020_add_shell_stub_type.go | 44 +++
pkg/types/backend.go | 1 +
pkg/worker/base_runc_config.json | 12 +-
proto/shell.pb.go | 243 ++++++++++++
proto/shell_grpc.pb.go | 109 ++++++
sdk/poetry.lock | 228 ++++++++++-
sdk/pyproject.toml | 1 +
sdk/src/beta9/abstractions/base/runner.py | 15 +
sdk/src/beta9/abstractions/mixins.py | 56 ++-
sdk/src/beta9/abstractions/shell.py | 342 ++++++++++++++++
sdk/src/beta9/cli/main.py | 2 +
sdk/src/beta9/cli/shell.py | 79 ++++
sdk/src/beta9/clients/shell/__init__.py | 46 +++
24 files changed, 1701 insertions(+), 15 deletions(-)
create mode 100644 pkg/abstractions/shell/http.go
create mode 100644 pkg/abstractions/shell/shell.go
create mode 100644 pkg/abstractions/shell/shell.proto
create mode 100644 pkg/repository/backend_postgres_migrations/020_add_shell_stub_type.go
create mode 100644 proto/shell.pb.go
create mode 100644 proto/shell_grpc.pb.go
create mode 100644 sdk/src/beta9/abstractions/shell.py
create mode 100644 sdk/src/beta9/cli/shell.py
create mode 100644 sdk/src/beta9/clients/shell/__init__.py
diff --git a/bin/gen_proto.sh b/bin/gen_proto.sh
index 9011d4a8d..246e50a4b 100755
--- a/bin/gen_proto.sh
+++ b/bin/gen_proto.sh
@@ -43,3 +43,6 @@ protoc -I ./pkg/abstractions/experimental/signal/ --python_betterproto_beta9_out
protoc -I ./pkg/abstractions/experimental/bot/ --go_out=./proto --go_opt=paths=source_relative --go-grpc_out=./proto --go-grpc_opt=paths=source_relative ./pkg/abstractions/experimental/bot/bot.proto
protoc -I ./pkg/abstractions/experimental/bot/ --python_betterproto_beta9_out=./sdk/src/beta9/clients/ ./pkg/abstractions/experimental/bot/bot.proto
+
+protoc -I ./pkg/abstractions/shell/ --go_out=./proto --go_opt=paths=source_relative --go-grpc_out=./proto --go-grpc_opt=paths=source_relative ./pkg/abstractions/shell/shell.proto
+protoc -I ./pkg/abstractions/shell/ --python_betterproto_beta9_out=./sdk/src/beta9/clients/ ./pkg/abstractions/shell/shell.proto
diff --git a/docker/Dockerfile.runner b/docker/Dockerfile.runner
index eff303e63..e49b4f13e 100644
--- a/docker/Dockerfile.runner
+++ b/docker/Dockerfile.runner
@@ -6,7 +6,7 @@ ENV DEBIAN_FRONTEND=noninteractive
RUN <> "/root/.profile";
+ echo "cd /mnt/code" >> "/root/.profile";
+ echo "export TERM=xterm-256color" >> "/root/.bashrc";
+ echo "alias ls='ls --color=auto'" >> "/root/.bashrc";
+ echo "alias ll='ls -lart --color=auto'" >> "/root/.bashrc";
+ sed -i 's/^#PasswordAuthentication.*/PasswordAuthentication yes/' /etc/ssh/sshd_config;
+ sed -i 's/^#PermitRootLogin.*/PermitRootLogin yes/' /etc/ssh/sshd_config;
+ sed -i 's/^#PubkeyAuthentication.*/PubkeyAuthentication no/' /etc/ssh/sshd_config;
+ echo "AllowUsers $USERNAME" >> /etc/ssh/sshd_config;
+ echo "PermitUserEnvironment yes" >> /etc/ssh/sshd_config;
+ printenv > /etc/environment;
+ exec /usr/sbin/sshd -D -p 8001
+ `, token)
+
+ entryPoint := []string{
+ "/bin/bash",
+ "-c",
+ startupCommand,
+ }
+
+ err = ss.rdb.Set(ctx, Keys.shellContainerTTL(containerId), "1", time.Duration(shellContainerTtlS)*time.Second).Err()
+ if err != nil {
+ return &pb.CreateShellResponse{
+ Ok: false,
+ ErrMsg: "Failed to set shell container ttl",
+ }, nil
+ }
+
+ err = ss.scheduler.Run(&types.ContainerRequest{
+ ContainerId: containerId,
+ Env: env,
+ Cpu: stubConfig.Runtime.Cpu,
+ Memory: stubConfig.Runtime.Memory,
+ GpuRequest: gpuRequest,
+ GpuCount: uint32(gpuCount),
+ ImageId: stubConfig.Runtime.ImageId,
+ StubId: stub.ExternalId,
+ WorkspaceId: authInfo.Workspace.ExternalId,
+ Workspace: *authInfo.Workspace,
+ EntryPoint: entryPoint,
+ Mounts: mounts,
+ Stub: *stub,
+ })
+ if err != nil {
+ return &pb.CreateShellResponse{
+ Ok: false,
+ ErrMsg: "Failed to run shell container",
+ }, nil
+ }
+
+ err = ss.waitForContainer(ctx, containerId, containerWaitTimeoutDurationS)
+ if err != nil {
+ ss.scheduler.Stop(&types.StopContainerArgs{
+ ContainerId: containerId,
+ Force: true,
+ })
+
+ return &pb.CreateShellResponse{
+ Ok: false,
+ ErrMsg: "Failed to wait for shell container",
+ }, nil
+ }
+
+ return &pb.CreateShellResponse{
+ Ok: true,
+ ContainerId: containerId,
+ Token: token,
+ }, nil
+}
+
+func (ss *SSHShellService) genContainerId(stubId string) string {
+ return fmt.Sprintf("%s-%s-%s", shellContainerPrefix, stubId, uuid.New().String()[:8])
+}
+
+func (ss *SSHShellService) keepAlive(ctx context.Context, containerId string, done <-chan struct{}) {
+ ticker := time.NewTicker(containerKeepAliveIntervalS)
+ defer ticker.Stop()
+
+ for {
+ select {
+ case <-ss.ctx.Done():
+ return
+ case <-ctx.Done():
+ return
+ case <-done:
+ return
+ case <-ticker.C:
+ ss.rdb.Set(ctx, Keys.shellContainerTTL(containerId), "1", time.Duration(shellContainerTtlS)*time.Second).Err()
+ }
+ }
+}
+
+func (ss *SSHShellService) waitForContainer(ctx context.Context, containerId string, timeout time.Duration) error {
+ timeoutCtx, cancel := context.WithTimeout(ctx, timeout)
+ defer cancel()
+
+ for {
+ select {
+ case <-ss.ctx.Done():
+ return nil
+ case <-timeoutCtx.Done():
+ return fmt.Errorf("timed out waiting for container to be available")
+ default:
+ containerState, err := ss.containerRepo.GetContainerState(containerId)
+ if err != nil {
+ return err
+ }
+
+ if containerState.Status == types.ContainerStatusRunning {
+ return nil
+ }
+
+ time.Sleep(containerWaitPollIntervalS)
+ }
+ }
+}
+
+func generateToken(length int) (string, error) {
+ byteLength := (length*6 + 7) / 8 // Calculate the number of bytes needed
+
+ randomBytes := make([]byte, byteLength)
+ _, err := rand.Read(randomBytes)
+ if err != nil {
+ return "", err
+ }
+
+ token := base64.URLEncoding.EncodeToString(randomBytes)
+ return token[:length], nil
+}
+
+// Redis keys
+var (
+ shellContainerTTL string = "shell:container_ttl:%s"
+)
+
+var Keys = &keys{}
+
+type keys struct{}
+
+func (k *keys) shellContainerTTL(containerId string) string {
+ return fmt.Sprintf(shellContainerTTL, containerId)
+}
diff --git a/pkg/abstractions/shell/shell.proto b/pkg/abstractions/shell/shell.proto
new file mode 100644
index 000000000..05de38f2f
--- /dev/null
+++ b/pkg/abstractions/shell/shell.proto
@@ -0,0 +1,18 @@
+syntax = "proto3";
+
+option go_package = "github.com/beam-cloud/beta9/proto";
+
+package shell;
+
+service ShellService {
+ rpc CreateShell(CreateShellRequest) returns (CreateShellResponse) {}
+}
+
+message CreateShellRequest { string stub_id = 1; }
+
+message CreateShellResponse {
+ bool ok = 1;
+ string container_id = 2;
+ string token = 3;
+ string err_msg = 4;
+}
diff --git a/pkg/api/v1/stub.go b/pkg/api/v1/stub.go
index 00fb25e57..67ee5b1b6 100644
--- a/pkg/api/v1/stub.go
+++ b/pkg/api/v1/stub.go
@@ -115,7 +115,7 @@ func (g *StubGroup) GetURL(ctx echo.Context) error {
// Get URL for Serves
if stub.Type.IsServe() {
- invokeUrl := common.BuildServeURL(g.config.GatewayService.HTTP.GetExternalURL(), filter.URLType, stub)
+ invokeUrl := common.BuildStubURL(g.config.GatewayService.HTTP.GetExternalURL(), filter.URLType, stub)
return ctx.JSON(http.StatusOK, map[string]string{"url": invokeUrl})
}
diff --git a/pkg/common/key_events.go b/pkg/common/key_events.go
index cdd5c082b..9102805a6 100644
--- a/pkg/common/key_events.go
+++ b/pkg/common/key_events.go
@@ -49,6 +49,10 @@ func (kem *KeyEventManager) fetchExistingKeys(patternPrefix string) ([]string, e
return trimmedKeys, nil
}
+func (kem *KeyEventManager) TrimKeyspacePrefix(key string) string {
+ return strings.TrimPrefix(key, keyspacePrefix)
+}
+
func (kem *KeyEventManager) ListenForPattern(ctx context.Context, patternPrefix string, keyEventChan chan KeyEvent) error {
existingKeys, err := kem.fetchExistingKeys(patternPrefix)
if err != nil {
diff --git a/pkg/common/url.go b/pkg/common/url.go
index f713a8200..0287f20f4 100644
--- a/pkg/common/url.go
+++ b/pkg/common/url.go
@@ -31,7 +31,7 @@ func BuildDeploymentURL(externalUrl, urlType string, stub *types.StubWithRelated
return fmt.Sprintf("%s://%s/%s/%s/v%d", parsedUrl.Scheme, parsedUrl.Host, stub.Type.Kind(), deployment.Name, deployment.Version)
}
-func BuildServeURL(externalUrl, urlType string, stub *types.StubWithRelated) string {
+func BuildStubURL(externalUrl, urlType string, stub *types.StubWithRelated) string {
parsedUrl, err := url.Parse(externalUrl)
if err != nil {
return ""
diff --git a/pkg/common/url_test.go b/pkg/common/url_test.go
index e85b56de9..8cc05095d 100644
--- a/pkg/common/url_test.go
+++ b/pkg/common/url_test.go
@@ -76,6 +76,15 @@ func TestBuildServeURL(t *testing.T) {
invokeType: InvokeUrlTypeHost,
expectedUrl: "http://e9c29586-c465-4a67-9c9b-25293d1ce77b.app.example.com",
},
+ {
+ name: "returns host-based URL",
+ stub: &types.StubWithRelated{Stub: types.Stub{
+ ExternalId: "fbedeff-c465-4a67-9c9b-25293d1ce77b",
+ Type: types.StubType(types.StubTypeShell),
+ }},
+ invokeType: InvokeUrlTypeHost,
+ expectedUrl: "http://fbedeff-c465-4a67-9c9b-25293d1ce77b.app.example.com",
+ },
{
name: "returns path-based URL",
stub: &types.StubWithRelated{Stub: types.Stub{
@@ -89,7 +98,7 @@ func TestBuildServeURL(t *testing.T) {
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
- got := BuildServeURL(externalUrl, test.invokeType, test.stub)
+ got := BuildStubURL(externalUrl, test.invokeType, test.stub)
assert.Equal(t, test.expectedUrl, got)
})
}
diff --git a/pkg/gateway/gateway.go b/pkg/gateway/gateway.go
index 1a2ff75bf..8c5b7f7a2 100644
--- a/pkg/gateway/gateway.go
+++ b/pkg/gateway/gateway.go
@@ -22,6 +22,7 @@ import (
"github.com/beam-cloud/beta9/pkg/abstractions/endpoint"
bot "github.com/beam-cloud/beta9/pkg/abstractions/experimental/bot"
_signal "github.com/beam-cloud/beta9/pkg/abstractions/experimental/signal"
+ _shell "github.com/beam-cloud/beta9/pkg/abstractions/shell"
"github.com/beam-cloud/beta9/pkg/abstractions/function"
"github.com/beam-cloud/beta9/pkg/abstractions/image"
@@ -376,6 +377,23 @@ func (g *Gateway) registerServices() error {
}
pb.RegisterBotServiceServer(g.grpcServer, botService)
+ // Register shell service
+ ss, err := _shell.NewSSHShellService(g.ctx, _shell.ShellServiceOpts{
+ Config: g.Config,
+ RedisClient: g.RedisClient,
+ Scheduler: g.Scheduler,
+ BackendRepo: g.BackendRepo,
+ WorkspaceRepo: g.WorkspaceRepo,
+ ContainerRepo: g.ContainerRepo,
+ Tailscale: g.Tailscale,
+ EventRepo: g.EventRepo,
+ RouteGroup: g.rootRouteGroup,
+ })
+ if err != nil {
+ return err
+ }
+ pb.RegisterShellServiceServer(g.grpcServer, ss)
+
// Register scheduler
s, err := scheduler.NewSchedulerService(g.Scheduler)
if err != nil {
diff --git a/pkg/gateway/services/stub.go b/pkg/gateway/services/stub.go
index b8bf9b9d0..7c440b0d5 100644
--- a/pkg/gateway/services/stub.go
+++ b/pkg/gateway/services/stub.go
@@ -255,9 +255,9 @@ func (gws *GatewayService) GetURL(ctx context.Context, in *pb.GetURLRequest) (*p
in.UrlType = gws.appConfig.GatewayService.InvokeURLType
}
- // Get URL for Serves
- if stub.Type.IsServe() {
- invokeUrl := common.BuildServeURL(gws.appConfig.GatewayService.HTTP.GetExternalURL(), in.UrlType, stub)
+ // Get URL for Serves or Shells
+ if stub.Type.IsServe() || stub.Type.Kind() == types.StubTypeShell {
+ invokeUrl := common.BuildStubURL(gws.appConfig.GatewayService.HTTP.GetExternalURL(), in.UrlType, stub)
return &pb.GetURLResponse{
Ok: true,
Url: invokeUrl,
diff --git a/pkg/repository/backend_postgres_migrations/020_add_shell_stub_type.go b/pkg/repository/backend_postgres_migrations/020_add_shell_stub_type.go
new file mode 100644
index 000000000..d32f96995
--- /dev/null
+++ b/pkg/repository/backend_postgres_migrations/020_add_shell_stub_type.go
@@ -0,0 +1,44 @@
+package backend_postgres_migrations
+
+import (
+ "context"
+ "database/sql"
+ "fmt"
+
+ "github.com/pressly/goose/v3"
+)
+
+func init() {
+ goose.AddMigrationContext(upAddShellStubType, downRemoveShellStubType)
+}
+
+func upAddShellStubType(ctx context.Context, tx *sql.Tx) error {
+ newStubTypes := []string{"shell"}
+
+ for _, stubType := range newStubTypes {
+ addEnumSQL := fmt.Sprintf(`
+DO $$
+BEGIN
+ IF NOT EXISTS (
+ SELECT 1 FROM pg_type t
+ JOIN pg_enum e ON t.oid = e.enumtypid
+ JOIN pg_catalog.pg_namespace n ON n.oid = t.typnamespace
+ WHERE n.nspname = 'public' AND t.typname = 'stub_type' AND e.enumlabel = '%s'
+ ) THEN
+ EXECUTE 'ALTER TYPE stub_type ADD VALUE ' || quote_literal('%s');
+ END IF;
+END
+$$;`, stubType, stubType)
+
+ if _, err := tx.Exec(addEnumSQL); err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
+func downRemoveShellStubType(ctx context.Context, tx *sql.Tx) error {
+ // PostgreSQL doesn't support removing values from an ENUM directly
+ return nil
+}
diff --git a/pkg/types/backend.go b/pkg/types/backend.go
index 6a324b2fa..de64e2f29 100644
--- a/pkg/types/backend.go
+++ b/pkg/types/backend.go
@@ -208,6 +208,7 @@ const (
StubTypeFunctionDeployment string = "function/deployment"
StubTypeFunctionServe string = "function/serve"
StubTypeContainer string = "container"
+ StubTypeShell string = "shell"
StubTypeTaskQueue string = "taskqueue"
StubTypeTaskQueueDeployment string = "taskqueue/deployment"
StubTypeTaskQueueServe string = "taskqueue/serve"
diff --git a/pkg/worker/base_runc_config.json b/pkg/worker/base_runc_config.json
index adaa9562f..40e42414a 100644
--- a/pkg/worker/base_runc_config.json
+++ b/pkg/worker/base_runc_config.json
@@ -30,7 +30,8 @@
"CAP_FOWNER",
"CAP_SETGID",
"CAP_SETUID",
- "CAP_SETFCAP"
+ "CAP_SETFCAP",
+ "CAP_SYS_CHROOT"
],
"effective": [
"CAP_AUDIT_WRITE",
@@ -42,7 +43,8 @@
"CAP_FOWNER",
"CAP_SETGID",
"CAP_SETUID",
- "CAP_SETFCAP"
+ "CAP_SETFCAP",
+ "CAP_SYS_CHROOT"
],
"permitted": [
"CAP_AUDIT_WRITE",
@@ -54,7 +56,8 @@
"CAP_FOWNER",
"CAP_SETGID",
"CAP_SETUID",
- "CAP_SETFCAP"
+ "CAP_SETFCAP",
+ "CAP_SYS_CHROOT"
],
"ambient": [
"CAP_AUDIT_WRITE",
@@ -66,7 +69,8 @@
"CAP_FOWNER",
"CAP_SETGID",
"CAP_SETUID",
- "CAP_SETFCAP"
+ "CAP_SETFCAP",
+ "CAP_SYS_CHROOT"
]
},
"rlimits": [],
diff --git a/proto/shell.pb.go b/proto/shell.pb.go
new file mode 100644
index 000000000..1e1d02c11
--- /dev/null
+++ b/proto/shell.pb.go
@@ -0,0 +1,243 @@
+// Code generated by protoc-gen-go. DO NOT EDIT.
+// versions:
+// protoc-gen-go v1.31.0
+// protoc v4.25.1
+// source: shell.proto
+
+package proto
+
+import (
+ protoreflect "google.golang.org/protobuf/reflect/protoreflect"
+ protoimpl "google.golang.org/protobuf/runtime/protoimpl"
+ reflect "reflect"
+ sync "sync"
+)
+
+const (
+ // Verify that this generated code is sufficiently up-to-date.
+ _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
+ // Verify that runtime/protoimpl is sufficiently up-to-date.
+ _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
+)
+
+type CreateShellRequest struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ StubId string `protobuf:"bytes,1,opt,name=stub_id,json=stubId,proto3" json:"stub_id,omitempty"`
+}
+
+func (x *CreateShellRequest) Reset() {
+ *x = CreateShellRequest{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_shell_proto_msgTypes[0]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *CreateShellRequest) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*CreateShellRequest) ProtoMessage() {}
+
+func (x *CreateShellRequest) ProtoReflect() protoreflect.Message {
+ mi := &file_shell_proto_msgTypes[0]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use CreateShellRequest.ProtoReflect.Descriptor instead.
+func (*CreateShellRequest) Descriptor() ([]byte, []int) {
+ return file_shell_proto_rawDescGZIP(), []int{0}
+}
+
+func (x *CreateShellRequest) GetStubId() string {
+ if x != nil {
+ return x.StubId
+ }
+ return ""
+}
+
+type CreateShellResponse struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ Ok bool `protobuf:"varint,1,opt,name=ok,proto3" json:"ok,omitempty"`
+ ContainerId string `protobuf:"bytes,2,opt,name=container_id,json=containerId,proto3" json:"container_id,omitempty"`
+ Token string `protobuf:"bytes,3,opt,name=token,proto3" json:"token,omitempty"`
+ ErrMsg string `protobuf:"bytes,4,opt,name=err_msg,json=errMsg,proto3" json:"err_msg,omitempty"`
+}
+
+func (x *CreateShellResponse) Reset() {
+ *x = CreateShellResponse{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_shell_proto_msgTypes[1]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *CreateShellResponse) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*CreateShellResponse) ProtoMessage() {}
+
+func (x *CreateShellResponse) ProtoReflect() protoreflect.Message {
+ mi := &file_shell_proto_msgTypes[1]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use CreateShellResponse.ProtoReflect.Descriptor instead.
+func (*CreateShellResponse) Descriptor() ([]byte, []int) {
+ return file_shell_proto_rawDescGZIP(), []int{1}
+}
+
+func (x *CreateShellResponse) GetOk() bool {
+ if x != nil {
+ return x.Ok
+ }
+ return false
+}
+
+func (x *CreateShellResponse) GetContainerId() string {
+ if x != nil {
+ return x.ContainerId
+ }
+ return ""
+}
+
+func (x *CreateShellResponse) GetToken() string {
+ if x != nil {
+ return x.Token
+ }
+ return ""
+}
+
+func (x *CreateShellResponse) GetErrMsg() string {
+ if x != nil {
+ return x.ErrMsg
+ }
+ return ""
+}
+
+var File_shell_proto protoreflect.FileDescriptor
+
+var file_shell_proto_rawDesc = []byte{
+ 0x0a, 0x0b, 0x73, 0x68, 0x65, 0x6c, 0x6c, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x05, 0x73,
+ 0x68, 0x65, 0x6c, 0x6c, 0x22, 0x2d, 0x0a, 0x12, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x53, 0x68,
+ 0x65, 0x6c, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x17, 0x0a, 0x07, 0x73, 0x74,
+ 0x75, 0x62, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x74, 0x75,
+ 0x62, 0x49, 0x64, 0x22, 0x77, 0x0a, 0x13, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x53, 0x68, 0x65,
+ 0x6c, 0x6c, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x6f, 0x6b,
+ 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x02, 0x6f, 0x6b, 0x12, 0x21, 0x0a, 0x0c, 0x63, 0x6f,
+ 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09,
+ 0x52, 0x0b, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x49, 0x64, 0x12, 0x14, 0x0a,
+ 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x74, 0x6f,
+ 0x6b, 0x65, 0x6e, 0x12, 0x17, 0x0a, 0x07, 0x65, 0x72, 0x72, 0x5f, 0x6d, 0x73, 0x67, 0x18, 0x04,
+ 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x65, 0x72, 0x72, 0x4d, 0x73, 0x67, 0x32, 0x56, 0x0a, 0x0c,
+ 0x53, 0x68, 0x65, 0x6c, 0x6c, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x46, 0x0a, 0x0b,
+ 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x53, 0x68, 0x65, 0x6c, 0x6c, 0x12, 0x19, 0x2e, 0x73, 0x68,
+ 0x65, 0x6c, 0x6c, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x53, 0x68, 0x65, 0x6c, 0x6c, 0x52,
+ 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1a, 0x2e, 0x73, 0x68, 0x65, 0x6c, 0x6c, 0x2e, 0x43,
+ 0x72, 0x65, 0x61, 0x74, 0x65, 0x53, 0x68, 0x65, 0x6c, 0x6c, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e,
+ 0x73, 0x65, 0x22, 0x00, 0x42, 0x23, 0x5a, 0x21, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63,
+ 0x6f, 0x6d, 0x2f, 0x62, 0x65, 0x61, 0x6d, 0x2d, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2f, 0x62, 0x65,
+ 0x74, 0x61, 0x39, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f,
+ 0x33,
+}
+
+var (
+ file_shell_proto_rawDescOnce sync.Once
+ file_shell_proto_rawDescData = file_shell_proto_rawDesc
+)
+
+func file_shell_proto_rawDescGZIP() []byte {
+ file_shell_proto_rawDescOnce.Do(func() {
+ file_shell_proto_rawDescData = protoimpl.X.CompressGZIP(file_shell_proto_rawDescData)
+ })
+ return file_shell_proto_rawDescData
+}
+
+var file_shell_proto_msgTypes = make([]protoimpl.MessageInfo, 2)
+var file_shell_proto_goTypes = []interface{}{
+ (*CreateShellRequest)(nil), // 0: shell.CreateShellRequest
+ (*CreateShellResponse)(nil), // 1: shell.CreateShellResponse
+}
+var file_shell_proto_depIdxs = []int32{
+ 0, // 0: shell.ShellService.CreateShell:input_type -> shell.CreateShellRequest
+ 1, // 1: shell.ShellService.CreateShell:output_type -> shell.CreateShellResponse
+ 1, // [1:2] is the sub-list for method output_type
+ 0, // [0:1] is the sub-list for method input_type
+ 0, // [0:0] is the sub-list for extension type_name
+ 0, // [0:0] is the sub-list for extension extendee
+ 0, // [0:0] is the sub-list for field type_name
+}
+
+func init() { file_shell_proto_init() }
+func file_shell_proto_init() {
+ if File_shell_proto != nil {
+ return
+ }
+ if !protoimpl.UnsafeEnabled {
+ file_shell_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*CreateShellRequest); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_shell_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*CreateShellResponse); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ }
+ type x struct{}
+ out := protoimpl.TypeBuilder{
+ File: protoimpl.DescBuilder{
+ GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
+ RawDescriptor: file_shell_proto_rawDesc,
+ NumEnums: 0,
+ NumMessages: 2,
+ NumExtensions: 0,
+ NumServices: 1,
+ },
+ GoTypes: file_shell_proto_goTypes,
+ DependencyIndexes: file_shell_proto_depIdxs,
+ MessageInfos: file_shell_proto_msgTypes,
+ }.Build()
+ File_shell_proto = out.File
+ file_shell_proto_rawDesc = nil
+ file_shell_proto_goTypes = nil
+ file_shell_proto_depIdxs = nil
+}
diff --git a/proto/shell_grpc.pb.go b/proto/shell_grpc.pb.go
new file mode 100644
index 000000000..639ed515f
--- /dev/null
+++ b/proto/shell_grpc.pb.go
@@ -0,0 +1,109 @@
+// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
+// versions:
+// - protoc-gen-go-grpc v1.3.0
+// - protoc v4.25.1
+// source: shell.proto
+
+package proto
+
+import (
+ context "context"
+ grpc "google.golang.org/grpc"
+ codes "google.golang.org/grpc/codes"
+ status "google.golang.org/grpc/status"
+)
+
+// This is a compile-time assertion to ensure that this generated file
+// is compatible with the grpc package it is being compiled against.
+// Requires gRPC-Go v1.32.0 or later.
+const _ = grpc.SupportPackageIsVersion7
+
+const (
+ ShellService_CreateShell_FullMethodName = "/shell.ShellService/CreateShell"
+)
+
+// ShellServiceClient is the client API for ShellService service.
+//
+// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
+type ShellServiceClient interface {
+ CreateShell(ctx context.Context, in *CreateShellRequest, opts ...grpc.CallOption) (*CreateShellResponse, error)
+}
+
+type shellServiceClient struct {
+ cc grpc.ClientConnInterface
+}
+
+func NewShellServiceClient(cc grpc.ClientConnInterface) ShellServiceClient {
+ return &shellServiceClient{cc}
+}
+
+func (c *shellServiceClient) CreateShell(ctx context.Context, in *CreateShellRequest, opts ...grpc.CallOption) (*CreateShellResponse, error) {
+ out := new(CreateShellResponse)
+ err := c.cc.Invoke(ctx, ShellService_CreateShell_FullMethodName, in, out, opts...)
+ if err != nil {
+ return nil, err
+ }
+ return out, nil
+}
+
+// ShellServiceServer is the server API for ShellService service.
+// All implementations must embed UnimplementedShellServiceServer
+// for forward compatibility
+type ShellServiceServer interface {
+ CreateShell(context.Context, *CreateShellRequest) (*CreateShellResponse, error)
+ mustEmbedUnimplementedShellServiceServer()
+}
+
+// UnimplementedShellServiceServer must be embedded to have forward compatible implementations.
+type UnimplementedShellServiceServer struct {
+}
+
+func (UnimplementedShellServiceServer) CreateShell(context.Context, *CreateShellRequest) (*CreateShellResponse, error) {
+ return nil, status.Errorf(codes.Unimplemented, "method CreateShell not implemented")
+}
+func (UnimplementedShellServiceServer) mustEmbedUnimplementedShellServiceServer() {}
+
+// UnsafeShellServiceServer may be embedded to opt out of forward compatibility for this service.
+// Use of this interface is not recommended, as added methods to ShellServiceServer will
+// result in compilation errors.
+type UnsafeShellServiceServer interface {
+ mustEmbedUnimplementedShellServiceServer()
+}
+
+func RegisterShellServiceServer(s grpc.ServiceRegistrar, srv ShellServiceServer) {
+ s.RegisterService(&ShellService_ServiceDesc, srv)
+}
+
+func _ShellService_CreateShell_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+ in := new(CreateShellRequest)
+ if err := dec(in); err != nil {
+ return nil, err
+ }
+ if interceptor == nil {
+ return srv.(ShellServiceServer).CreateShell(ctx, in)
+ }
+ info := &grpc.UnaryServerInfo{
+ Server: srv,
+ FullMethod: ShellService_CreateShell_FullMethodName,
+ }
+ handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+ return srv.(ShellServiceServer).CreateShell(ctx, req.(*CreateShellRequest))
+ }
+ return interceptor(ctx, in, info, handler)
+}
+
+// ShellService_ServiceDesc is the grpc.ServiceDesc for ShellService service.
+// It's only intended for direct use with grpc.RegisterService,
+// and not to be introspected or modified (even as a copy)
+var ShellService_ServiceDesc = grpc.ServiceDesc{
+ ServiceName: "shell.ShellService",
+ HandlerType: (*ShellServiceServer)(nil),
+ Methods: []grpc.MethodDesc{
+ {
+ MethodName: "CreateShell",
+ Handler: _ShellService_CreateShell_Handler,
+ },
+ },
+ Streams: []grpc.StreamDesc{},
+ Metadata: "shell.proto",
+}
diff --git a/sdk/poetry.lock b/sdk/poetry.lock
index 82e387727..9477f5a37 100644
--- a/sdk/poetry.lock
+++ b/sdk/poetry.lock
@@ -1,4 +1,4 @@
-# This file is automatically @generated by Poetry 1.8.5 and should not be changed by hand.
+# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand.
[[package]]
name = "annotated-types"
@@ -72,6 +72,44 @@ tests = ["attrs[tests-no-zope]", "zope-interface"]
tests-mypy = ["mypy (>=1.6)", "pytest-mypy-plugins"]
tests-no-zope = ["attrs[tests-mypy]", "cloudpickle", "hypothesis", "pympler", "pytest (>=4.3.0)", "pytest-xdist[psutil]"]
+[[package]]
+name = "bcrypt"
+version = "4.2.1"
+description = "Modern password hashing for your software and your servers"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "bcrypt-4.2.1-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:1340411a0894b7d3ef562fb233e4b6ed58add185228650942bdc885362f32c17"},
+ {file = "bcrypt-4.2.1-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b1ee315739bc8387aa36ff127afc99120ee452924e0df517a8f3e4c0187a0f5f"},
+ {file = "bcrypt-4.2.1-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8dbd0747208912b1e4ce730c6725cb56c07ac734b3629b60d4398f082ea718ad"},
+ {file = "bcrypt-4.2.1-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:aaa2e285be097050dba798d537b6efd9b698aa88eef52ec98d23dcd6d7cf6fea"},
+ {file = "bcrypt-4.2.1-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:76d3e352b32f4eeb34703370e370997065d28a561e4a18afe4fef07249cb4396"},
+ {file = "bcrypt-4.2.1-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:b7703ede632dc945ed1172d6f24e9f30f27b1b1a067f32f68bf169c5f08d0425"},
+ {file = "bcrypt-4.2.1-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:89df2aea2c43be1e1fa066df5f86c8ce822ab70a30e4c210968669565c0f4685"},
+ {file = "bcrypt-4.2.1-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:04e56e3fe8308a88b77e0afd20bec516f74aecf391cdd6e374f15cbed32783d6"},
+ {file = "bcrypt-4.2.1-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:cfdf3d7530c790432046c40cda41dfee8c83e29482e6a604f8930b9930e94139"},
+ {file = "bcrypt-4.2.1-cp37-abi3-win32.whl", hash = "sha256:adadd36274510a01f33e6dc08f5824b97c9580583bd4487c564fc4617b328005"},
+ {file = "bcrypt-4.2.1-cp37-abi3-win_amd64.whl", hash = "sha256:8c458cd103e6c5d1d85cf600e546a639f234964d0228909d8f8dbeebff82d526"},
+ {file = "bcrypt-4.2.1-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:8ad2f4528cbf0febe80e5a3a57d7a74e6635e41af1ea5675282a33d769fba413"},
+ {file = "bcrypt-4.2.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:909faa1027900f2252a9ca5dfebd25fc0ef1417943824783d1c8418dd7d6df4a"},
+ {file = "bcrypt-4.2.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cde78d385d5e93ece5479a0a87f73cd6fa26b171c786a884f955e165032b262c"},
+ {file = "bcrypt-4.2.1-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:533e7f3bcf2f07caee7ad98124fab7499cb3333ba2274f7a36cf1daee7409d99"},
+ {file = "bcrypt-4.2.1-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:687cf30e6681eeda39548a93ce9bfbb300e48b4d445a43db4298d2474d2a1e54"},
+ {file = "bcrypt-4.2.1-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:041fa0155c9004eb98a232d54da05c0b41d4b8e66b6fc3cb71b4b3f6144ba837"},
+ {file = "bcrypt-4.2.1-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:f85b1ffa09240c89aa2e1ae9f3b1c687104f7b2b9d2098da4e923f1b7082d331"},
+ {file = "bcrypt-4.2.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c6f5fa3775966cca251848d4d5393ab016b3afed251163c1436fefdec3b02c84"},
+ {file = "bcrypt-4.2.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:807261df60a8b1ccd13e6599c779014a362ae4e795f5c59747f60208daddd96d"},
+ {file = "bcrypt-4.2.1-cp39-abi3-win32.whl", hash = "sha256:b588af02b89d9fad33e5f98f7838bf590d6d692df7153647724a7f20c186f6bf"},
+ {file = "bcrypt-4.2.1-cp39-abi3-win_amd64.whl", hash = "sha256:e84e0e6f8e40a242b11bce56c313edc2be121cec3e0ec2d76fce01f6af33c07c"},
+ {file = "bcrypt-4.2.1-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:76132c176a6d9953cdc83c296aeaed65e1a708485fd55abf163e0d9f8f16ce0e"},
+ {file = "bcrypt-4.2.1-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:e158009a54c4c8bc91d5e0da80920d048f918c61a581f0a63e4e93bb556d362f"},
+ {file = "bcrypt-4.2.1.tar.gz", hash = "sha256:6765386e3ab87f569b276988742039baab087b2cdb01e809d74e74503c2faafe"},
+]
+
+[package.extras]
+tests = ["pytest (>=3.2.1,!=3.3.0)"]
+typecheck = ["mypy"]
+
[[package]]
name = "betterproto-beta9"
version = "2.0.0b7"
@@ -153,6 +191,85 @@ files = [
{file = "certifi-2024.12.14.tar.gz", hash = "sha256:b650d30f370c2b724812bee08008be0c4163b163ddaec3f2546c1caf65f191db"},
]
+[[package]]
+name = "cffi"
+version = "1.17.1"
+description = "Foreign Function Interface for Python calling C code."
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14"},
+ {file = "cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67"},
+ {file = "cffi-1.17.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382"},
+ {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702"},
+ {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3"},
+ {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6"},
+ {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17"},
+ {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8"},
+ {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e"},
+ {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be"},
+ {file = "cffi-1.17.1-cp310-cp310-win32.whl", hash = "sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c"},
+ {file = "cffi-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15"},
+ {file = "cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401"},
+ {file = "cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf"},
+ {file = "cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4"},
+ {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41"},
+ {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1"},
+ {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6"},
+ {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d"},
+ {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6"},
+ {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f"},
+ {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b"},
+ {file = "cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655"},
+ {file = "cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0"},
+ {file = "cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4"},
+ {file = "cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c"},
+ {file = "cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36"},
+ {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5"},
+ {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff"},
+ {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99"},
+ {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93"},
+ {file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3"},
+ {file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8"},
+ {file = "cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65"},
+ {file = "cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903"},
+ {file = "cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e"},
+ {file = "cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2"},
+ {file = "cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3"},
+ {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683"},
+ {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5"},
+ {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4"},
+ {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd"},
+ {file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed"},
+ {file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9"},
+ {file = "cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d"},
+ {file = "cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a"},
+ {file = "cffi-1.17.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:636062ea65bd0195bc012fea9321aca499c0504409f413dc88af450b57ffd03b"},
+ {file = "cffi-1.17.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7eac2ef9b63c79431bc4b25f1cd649d7f061a28808cbc6c47b534bd789ef964"},
+ {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e221cf152cff04059d011ee126477f0d9588303eb57e88923578ace7baad17f9"},
+ {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:31000ec67d4221a71bd3f67df918b1f88f676f1c3b535a7eb473255fdc0b83fc"},
+ {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f17be4345073b0a7b8ea599688f692ac3ef23ce28e5df79c04de519dbc4912c"},
+ {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2b1fac190ae3ebfe37b979cc1ce69c81f4e4fe5746bb401dca63a9062cdaf1"},
+ {file = "cffi-1.17.1-cp38-cp38-win32.whl", hash = "sha256:7596d6620d3fa590f677e9ee430df2958d2d6d6de2feeae5b20e82c00b76fbf8"},
+ {file = "cffi-1.17.1-cp38-cp38-win_amd64.whl", hash = "sha256:78122be759c3f8a014ce010908ae03364d00a1f81ab5c7f4a7a5120607ea56e1"},
+ {file = "cffi-1.17.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b2ab587605f4ba0bf81dc0cb08a41bd1c0a5906bd59243d56bad7668a6fc6c16"},
+ {file = "cffi-1.17.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:28b16024becceed8c6dfbc75629e27788d8a3f9030691a1dbf9821a128b22c36"},
+ {file = "cffi-1.17.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1d599671f396c4723d016dbddb72fe8e0397082b0a77a4fab8028923bec050e8"},
+ {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca74b8dbe6e8e8263c0ffd60277de77dcee6c837a3d0881d8c1ead7268c9e576"},
+ {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87"},
+ {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98e3969bcff97cae1b2def8ba499ea3d6f31ddfdb7635374834cf89a1a08ecf0"},
+ {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdf5ce3acdfd1661132f2a9c19cac174758dc2352bfe37d98aa7512c6b7178b3"},
+ {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9755e4345d1ec879e3849e62222a18c7174d65a6a92d5b346b1863912168b595"},
+ {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f1e22e8c4419538cb197e4dd60acc919d7696e5ef98ee4da4e01d3f8cfa4cc5a"},
+ {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c03e868a0b3bc35839ba98e74211ed2b05d2119be4e8a0f224fba9384f1fe02e"},
+ {file = "cffi-1.17.1-cp39-cp39-win32.whl", hash = "sha256:e31ae45bc2e29f6b2abd0de1cc3b9d5205aa847cafaecb8af1476a609a2f6eb7"},
+ {file = "cffi-1.17.1-cp39-cp39-win_amd64.whl", hash = "sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662"},
+ {file = "cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824"},
+]
+
+[package.dependencies]
+pycparser = "*"
+
[[package]]
name = "charset-normalizer"
version = "3.4.0"
@@ -405,6 +522,55 @@ files = [
python-dateutil = "*"
pytz = ">2021.1"
+[[package]]
+name = "cryptography"
+version = "43.0.3"
+description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers."
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "cryptography-43.0.3-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:bf7a1932ac4176486eab36a19ed4c0492da5d97123f1406cf15e41b05e787d2e"},
+ {file = "cryptography-43.0.3-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63efa177ff54aec6e1c0aefaa1a241232dcd37413835a9b674b6e3f0ae2bfd3e"},
+ {file = "cryptography-43.0.3-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e1ce50266f4f70bf41a2c6dc4358afadae90e2a1e5342d3c08883df1675374f"},
+ {file = "cryptography-43.0.3-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:443c4a81bb10daed9a8f334365fe52542771f25aedaf889fd323a853ce7377d6"},
+ {file = "cryptography-43.0.3-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:74f57f24754fe349223792466a709f8e0c093205ff0dca557af51072ff47ab18"},
+ {file = "cryptography-43.0.3-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9762ea51a8fc2a88b70cf2995e5675b38d93bf36bd67d91721c309df184f49bd"},
+ {file = "cryptography-43.0.3-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:81ef806b1fef6b06dcebad789f988d3b37ccaee225695cf3e07648eee0fc6b73"},
+ {file = "cryptography-43.0.3-cp37-abi3-win32.whl", hash = "sha256:cbeb489927bd7af4aa98d4b261af9a5bc025bd87f0e3547e11584be9e9427be2"},
+ {file = "cryptography-43.0.3-cp37-abi3-win_amd64.whl", hash = "sha256:f46304d6f0c6ab8e52770addfa2fc41e6629495548862279641972b6215451cd"},
+ {file = "cryptography-43.0.3-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:8ac43ae87929a5982f5948ceda07001ee5e83227fd69cf55b109144938d96984"},
+ {file = "cryptography-43.0.3-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:846da004a5804145a5f441b8530b4bf35afbf7da70f82409f151695b127213d5"},
+ {file = "cryptography-43.0.3-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f996e7268af62598f2fc1204afa98a3b5712313a55c4c9d434aef49cadc91d4"},
+ {file = "cryptography-43.0.3-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:f7b178f11ed3664fd0e995a47ed2b5ff0a12d893e41dd0494f406d1cf555cab7"},
+ {file = "cryptography-43.0.3-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:c2e6fc39c4ab499049df3bdf567f768a723a5e8464816e8f009f121a5a9f4405"},
+ {file = "cryptography-43.0.3-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:e1be4655c7ef6e1bbe6b5d0403526601323420bcf414598955968c9ef3eb7d16"},
+ {file = "cryptography-43.0.3-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:df6b6c6d742395dd77a23ea3728ab62f98379eff8fb61be2744d4679ab678f73"},
+ {file = "cryptography-43.0.3-cp39-abi3-win32.whl", hash = "sha256:d56e96520b1020449bbace2b78b603442e7e378a9b3bd68de65c782db1507995"},
+ {file = "cryptography-43.0.3-cp39-abi3-win_amd64.whl", hash = "sha256:0c580952eef9bf68c4747774cde7ec1d85a6e61de97281f2dba83c7d2c806362"},
+ {file = "cryptography-43.0.3-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:d03b5621a135bffecad2c73e9f4deb1a0f977b9a8ffe6f8e002bf6c9d07b918c"},
+ {file = "cryptography-43.0.3-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:a2a431ee15799d6db9fe80c82b055bae5a752bef645bba795e8e52687c69efe3"},
+ {file = "cryptography-43.0.3-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:281c945d0e28c92ca5e5930664c1cefd85efe80e5c0d2bc58dd63383fda29f83"},
+ {file = "cryptography-43.0.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:f18c716be16bc1fea8e95def49edf46b82fccaa88587a45f8dc0ff6ab5d8e0a7"},
+ {file = "cryptography-43.0.3-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:4a02ded6cd4f0a5562a8887df8b3bd14e822a90f97ac5e544c162899bc467664"},
+ {file = "cryptography-43.0.3-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:53a583b6637ab4c4e3591a15bc9db855b8d9dee9a669b550f311480acab6eb08"},
+ {file = "cryptography-43.0.3-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:1ec0bcf7e17c0c5669d881b1cd38c4972fade441b27bda1051665faaa89bdcaa"},
+ {file = "cryptography-43.0.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:2ce6fae5bdad59577b44e4dfed356944fbf1d925269114c28be377692643b4ff"},
+ {file = "cryptography-43.0.3.tar.gz", hash = "sha256:315b9001266a492a6ff443b61238f956b214dbec9910a081ba5b6646a055a805"},
+]
+
+[package.dependencies]
+cffi = {version = ">=1.12", markers = "platform_python_implementation != \"PyPy\""}
+
+[package.extras]
+docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=1.1.1)"]
+docstest = ["pyenchant (>=1.6.11)", "readme-renderer", "sphinxcontrib-spelling (>=4.0.1)"]
+nox = ["nox"]
+pep8test = ["check-sdist", "click", "mypy", "ruff"]
+sdist = ["build"]
+ssh = ["bcrypt (>=3.1.5)"]
+test = ["certifi", "cryptography-vectors (==43.0.3)", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"]
+test-randomorder = ["pytest-randomly"]
+
[[package]]
name = "docstring-parser"
version = "0.16"
@@ -888,6 +1054,27 @@ files = [
{file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"},
]
+[[package]]
+name = "paramiko"
+version = "3.5.0"
+description = "SSH2 protocol library"
+optional = false
+python-versions = ">=3.6"
+files = [
+ {file = "paramiko-3.5.0-py3-none-any.whl", hash = "sha256:1fedf06b085359051cd7d0d270cebe19e755a8a921cc2ddbfa647fb0cd7d68f9"},
+ {file = "paramiko-3.5.0.tar.gz", hash = "sha256:ad11e540da4f55cedda52931f1a3f812a8238a7af7f62a60de538cd80bb28124"},
+]
+
+[package.dependencies]
+bcrypt = ">=3.2"
+cryptography = ">=3.3"
+pynacl = ">=1.5"
+
+[package.extras]
+all = ["gssapi (>=1.4.1)", "invoke (>=2.0)", "pyasn1 (>=0.1.7)", "pywin32 (>=2.1.8)"]
+gssapi = ["gssapi (>=1.4.1)", "pyasn1 (>=0.1.7)", "pywin32 (>=2.1.8)"]
+invoke = ["invoke (>=2.0)"]
+
[[package]]
name = "pathspec"
version = "0.12.1"
@@ -964,6 +1151,17 @@ files = [
{file = "protobuf-4.25.5.tar.gz", hash = "sha256:7f8249476b4a9473645db7f8ab42b02fe1488cbe5fb72fddd445e0665afd8584"},
]
+[[package]]
+name = "pycparser"
+version = "2.22"
+description = "C parser in Python"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"},
+ {file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"},
+]
+
[[package]]
name = "pydantic"
version = "2.10.4"
@@ -1110,6 +1308,32 @@ files = [
[package.extras]
windows-terminal = ["colorama (>=0.4.6)"]
+[[package]]
+name = "pynacl"
+version = "1.5.0"
+description = "Python binding to the Networking and Cryptography (NaCl) library"
+optional = false
+python-versions = ">=3.6"
+files = [
+ {file = "PyNaCl-1.5.0-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:401002a4aaa07c9414132aaed7f6836ff98f59277a234704ff66878c2ee4a0d1"},
+ {file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:52cb72a79269189d4e0dc537556f4740f7f0a9ec41c1322598799b0bdad4ef92"},
+ {file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a36d4a9dda1f19ce6e03c9a784a2921a4b726b02e1c736600ca9c22029474394"},
+ {file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:0c84947a22519e013607c9be43706dd42513f9e6ae5d39d3613ca1e142fba44d"},
+ {file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06b8f6fa7f5de8d5d2f7573fe8c863c051225a27b61e6860fd047b1775807858"},
+ {file = "PyNaCl-1.5.0-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:a422368fc821589c228f4c49438a368831cb5bbc0eab5ebe1d7fac9dded6567b"},
+ {file = "PyNaCl-1.5.0-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:61f642bf2378713e2c2e1de73444a3778e5f0a38be6fee0fe532fe30060282ff"},
+ {file = "PyNaCl-1.5.0-cp36-abi3-win32.whl", hash = "sha256:e46dae94e34b085175f8abb3b0aaa7da40767865ac82c928eeb9e57e1ea8a543"},
+ {file = "PyNaCl-1.5.0-cp36-abi3-win_amd64.whl", hash = "sha256:20f42270d27e1b6a29f54032090b972d97f0a1b0948cc52392041ef7831fee93"},
+ {file = "PyNaCl-1.5.0.tar.gz", hash = "sha256:8ac7448f09ab85811607bdd21ec2464495ac8b7c66d146bf545b0f08fb9220ba"},
+]
+
+[package.dependencies]
+cffi = ">=1.4.1"
+
+[package.extras]
+docs = ["sphinx (>=1.6.5)", "sphinx-rtd-theme"]
+tests = ["hypothesis (>=3.27.0)", "pytest (>=3.2.1,!=3.3.0)"]
+
[[package]]
name = "pytest"
version = "8.3.4"
@@ -1480,4 +1704,4 @@ test = ["websockets"]
[metadata]
lock-version = "2.0"
python-versions = "^3.8"
-content-hash = "ea7a7e055c5235752d3e84ff318191eff7bfea0088b454eb9f8f1a0986be0361"
+content-hash = "1667031eb69e1901059f36bf0ab5c23c9686d9fb3fbcb23681d88f9fe09099c6"
diff --git a/sdk/pyproject.toml b/sdk/pyproject.toml
index 2c906cce0..611188dd5 100644
--- a/sdk/pyproject.toml
+++ b/sdk/pyproject.toml
@@ -27,6 +27,7 @@ croniter = "^3.0.3"
websocket-client = "^1.8.0"
prompt-toolkit = "^3.0.48"
requests = "^2.32.3"
+paramiko = "^3.5.0"
[tool.poetry.group.dev.dependencies]
pytest = "^8.2.0"
diff --git a/sdk/src/beta9/abstractions/base/runner.py b/sdk/src/beta9/abstractions/base/runner.py
index 59382e923..f58baa954 100644
--- a/sdk/src/beta9/abstractions/base/runner.py
+++ b/sdk/src/beta9/abstractions/base/runner.py
@@ -27,6 +27,7 @@
SecretVar,
)
from ...clients.gateway import TaskPolicy as TaskPolicyProto
+from ...clients.shell import ShellServiceStub
from ...config import ConfigContext, SDKSettings, get_config_context, get_settings
from ...env import called_on_import
from ...sync import FileSyncer, SyncEventHandler
@@ -46,12 +47,15 @@
ASGI_STUB_TYPE = "asgi"
SCHEDULE_STUB_TYPE = "schedule"
BOT_STUB_TYPE = "bot"
+SHELL_STUB_TYPE = "shell"
+
TASKQUEUE_DEPLOYMENT_STUB_TYPE = "taskqueue/deployment"
ENDPOINT_DEPLOYMENT_STUB_TYPE = "endpoint/deployment"
ASGI_DEPLOYMENT_STUB_TYPE = "asgi/deployment"
FUNCTION_DEPLOYMENT_STUB_TYPE = "function/deployment"
SCHEDULE_DEPLOYMENT_STUB_TYPE = "schedule/deployment"
BOT_DEPLOYMENT_STUB_TYPE = "bot/deployment"
+
TASKQUEUE_SERVE_STUB_TYPE = "taskqueue/serve"
ENDPOINT_SERVE_STUB_TYPE = "endpoint/serve"
ASGI_SERVE_STUB_TYPE = "asgi/serve"
@@ -141,6 +145,7 @@ def __init__(
self._map_callable_to_attr(attr="on_start", func=on_start)
self._gateway_stub: Optional[GatewayServiceStub] = None
+ self._shell_stub: Optional[ShellServiceStub] = None
self.syncer: FileSyncer = FileSyncer(self.gateway_stub)
self.settings: SDKSettings = get_settings()
self.config_context: ConfigContext = get_config_context()
@@ -209,6 +214,16 @@ def gateway_stub(self) -> GatewayServiceStub:
def gateway_stub(self, value) -> None:
self._gateway_stub = value
+ @property
+ def shell_stub(self) -> ShellServiceStub:
+ if not self._shell_stub:
+ self._shell_stub = ShellServiceStub(self.channel)
+ return self._shell_stub
+
+ @shell_stub.setter
+ def shell_stub(self, value) -> None:
+ self._shell_stub = value
+
def _parse_cpu_to_millicores(self, cpu: Union[float, str]) -> int:
"""
Parse the cpu argument to an integer value in millicores.
diff --git a/sdk/src/beta9/abstractions/mixins.py b/sdk/src/beta9/abstractions/mixins.py
index 39dd7c7c2..d9140f94c 100644
--- a/sdk/src/beta9/abstractions/mixins.py
+++ b/sdk/src/beta9/abstractions/mixins.py
@@ -1,16 +1,20 @@
+import urllib.parse
from typing import Any, Callable, ClassVar, Optional
from .. import terminal
-from ..clients.gateway import DeployStubRequest, DeployStubResponse
+from ..abstractions.base.runner import SHELL_STUB_TYPE
+from ..channel import with_grpc_error_handling
+from ..clients.gateway import DeployStubRequest, DeployStubResponse, GetUrlRequest, GetUrlResponse
+from ..clients.shell import CreateShellRequest
from ..config import ConfigContext
from .base.runner import RunnerAbstraction
+from .shell import SSHShell
class DeployableMixin:
func: Callable
parent: RunnerAbstraction
deployment_id: Optional[str] = None
-
deployment_stub_type: ClassVar[str]
def _validate(self):
@@ -60,3 +64,51 @@ def deploy(
self.parent.print_invocation_snippet(**invocation_details_options)
return deploy_response.ok
+
+ @with_grpc_error_handling
+ def shell(self, url_type: str = ""):
+ stub_type = SHELL_STUB_TYPE
+
+ if not self.parent.prepare_runtime(
+ func=self.func, stub_type=stub_type, force_create_stub=True
+ ):
+ return False
+
+ # First, spin up the shell container
+ with terminal.progress("Creating shell..."):
+ create_shell_response = self.parent.shell_stub.create_shell(
+ CreateShellRequest(
+ stub_id=self.parent.stub_id,
+ )
+ )
+ if not create_shell_response.ok:
+ return terminal.error(f"Failed to create shell: {create_shell_response.err_msg} ❌")
+
+ # Then, we can retrieve the URL and issue a CONNECT request / establish a tunnel
+ res: GetUrlResponse = self.parent.gateway_stub.get_url(
+ GetUrlRequest(
+ stub_id=self.parent.stub_id,
+ deployment_id=getattr(self, "deployment_id", ""),
+ url_type=url_type,
+ )
+ )
+ if not res.ok:
+ return terminal.error(f"Failed to get shell connection URL: {res.err_msg} ❌")
+
+ # Parse the URL to extract the container_id
+ parsed_url = urllib.parse.urlparse(res.url)
+ proxy_host, proxy_port = parsed_url.hostname, parsed_url.port
+ container_id = create_shell_response.container_id
+ ssh_token = create_shell_response.token
+
+ with SSHShell(
+ host=proxy_host,
+ port=proxy_port,
+ path=parsed_url.path,
+ container_id=container_id,
+ stub_id=self.parent.stub_id,
+ auth_token=self.parent.config_context.token,
+ username="root",
+ password=ssh_token,
+ ) as shell:
+ shell.start()
diff --git a/sdk/src/beta9/abstractions/shell.py b/sdk/src/beta9/abstractions/shell.py
new file mode 100644
index 000000000..03fdbe827
--- /dev/null
+++ b/sdk/src/beta9/abstractions/shell.py
@@ -0,0 +1,342 @@
+import os
+import socket
+import struct
+import sys
+import time
+import traceback
+from dataclasses import dataclass
+from typing import Optional
+
+from .. import terminal
+from ..env import is_local
+
+if is_local():
+ import paramiko
+
+
+def create_connect_tunnel(
+ proxy_host: str, proxy_port: int, path: str, stub_id: str, container_id: str, auth_token: str
+) -> socket.socket:
+ """
+ 1. Connect to the proxy_host:proxy_port over TCP.
+ 2. Send an HTTP CONNECT request for the correct path.
+ 3. If we get a 200 response, return the socket as a raw tunnel.
+ """
+
+ s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ s.connect((proxy_host, proxy_port))
+
+ # Construct the correct CONNECT request path
+ connect_path = f"{path}/{container_id}"
+
+ connect_req = (
+ f"CONNECT {connect_path} HTTP/1.1\r\n"
+ f"Host: {proxy_host}:{proxy_port}\r\n"
+ f"Proxy-Connection: Keep-Alive\r\n"
+ f"Authorization: Bearer {auth_token}\r\n"
+ f"\r\n"
+ )
+ s.sendall(connect_req.encode("ascii"))
+
+ response = b""
+ while b"\r\n\r\n" not in response:
+ chunk = s.recv(4096)
+ if not chunk:
+ break
+ response += chunk
+
+ response_str = response.decode("ascii", errors="replace")
+ if "200 OK" not in response_str:
+ s.close()
+ raise ConnectionError(f"CONNECT failed. Response:\n{response_str}")
+
+ # If we reach here, we have a raw TCP tunnel to "container_id"
+ return s
+
+
+@dataclass
+class SSHShell:
+ """Interactive ssh shell that can be used as a context manager - for use with 'shell' command"""
+
+ host: str
+ port: int
+ path: str
+ container_id: str
+ stub_id: str
+ auth_token: str
+ username: str
+ password: str
+ transport: Optional["paramiko.Transport"] = None
+
+ def _open(self):
+ self.socket: Optional[socket.socket] = None
+ self.channel: Optional["paramiko.Channel"] = None
+
+ try:
+ self.socket = create_connect_tunnel(
+ self.host,
+ self.port,
+ self.path,
+ self.stub_id,
+ self.container_id,
+ self.auth_token,
+ )
+ except BaseException:
+ terminal.error("Failed to establish ssh tunnel")
+
+ self.transport = paramiko.Transport(
+ self.socket
+ ) # Initialize a transport with the tunnel socket
+
+ self.transport.start_client()
+ self.transport.auth_password(self.username, self.password)
+ self.channel = self.transport.open_session()
+
+ # Get terminal size - https://stackoverflow.com/a/943921
+ rows, columns = os.popen("stty size", "r").read().split()
+
+ self.channel.get_pty(
+ term=os.getenv("TERM", "xterm-256color"), width=int(columns), height=int(rows)
+ )
+ self.channel.invoke_shell()
+
+ def _close(self):
+ if self.channel:
+ self.channel.close()
+
+ if self.transport:
+ self.transport.close()
+
+ if self.socket:
+ try:
+ # Set SO_LINGER to zero to forcefully close the socket
+ self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_LINGER, struct.pack("ii", 1, 0))
+ self.socket.shutdown(socket.SHUT_RDWR)
+ except OSError:
+ pass # Ignore any errors that occur after the socket is already closed
+ finally:
+ self.socket.close()
+
+ def __enter__(self):
+ try:
+ with terminal.progress("Connecting..."):
+ self._open()
+ except paramiko.SSHException:
+ self._close()
+ terminal.error(f"SSH error occurred: {traceback.format_exc()}")
+ except BaseException:
+ self._close()
+ terminal.error(f"Unexpected error occurred: {traceback.format_exc()}")
+
+ return self
+
+ def __exit__(self, exception_type, exception_value, traceback):
+ self._close()
+
+ def start(self):
+ """Start the interactive shell session."""
+ try:
+ interactive_shell(self.channel)
+
+ # Check the exit status after the shell session ends
+ exit_status = self.channel.recv_exit_status()
+ if exit_status != 0:
+ terminal.warn("Lost connection to shell, attempting to reconnect in 5 seconds...")
+ time.sleep(5)
+
+ with terminal.progress("Connecting..."):
+ self._open()
+
+ self.start()
+
+ except paramiko.SSHException:
+ self._close()
+ terminal.error(f"SSH error occurred in shell: {traceback.format_exc()}")
+ except BaseException:
+ self._close()
+ terminal.error(f"Unexpected error occurred in shell: {traceback.format_exc()}")
+
+
+"""
+ NOTE: much of the interactive shell code below is pulled from paramiko's examples, with a few slight modifications for use here.
+ Original license / source information is as follows:
+"""
+# Source: https://github.com/paramiko/paramiko/blob/main/demos/interactive.py
+
+# Copyright (C) 2003-2007 Robey Pointer
+#
+# This file is part of paramiko.
+#
+# Paramiko is free software; you can redistribute it and/or modify it under the
+# terms of the GNU Lesser General Public License as published by the Free
+# Software Foundation; either version 2.1 of the License, or (at your option)
+# any later version.
+#
+# Paramiko is distributed in the hope that it will be useful, but WITHOUT ANY
+# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+# A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
+# details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with Paramiko; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+
+# https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-Bracketed-Paste-Mode
+START_PASTE = "\x1b\x5b\x32\x30\x30\x7e" # ESC[200~
+END_PASTE = "\x1b\x5b\x32\x30\x31\x7e" # ESC[201~
+
+# windows does not have termios...
+try:
+ import termios
+ import tty
+
+ has_termios = True
+except ImportError:
+ has_termios = False
+
+
+def is_int(val: str) -> bool:
+ try:
+ int(val)
+ return True
+ except Exception:
+ return False
+
+
+def interactive_shell(chan: "paramiko.Channel"):
+ if has_termios:
+ posix_shell(chan)
+ else:
+ windows_shell(chan)
+
+
+def posix_readkey() -> str:
+ """Get a keypress. If an escaped key is pressed, the full sequence is
+ read and returned.
+
+ Copied from readchar:
+ https://github.com/magmax/python-readchar/blob/master/readchar/_posix_read.py#L30
+ """
+
+ c1 = sys.stdin.read(1)
+
+ if c1 != "\x1b": # ESC
+ return c1
+
+ c2 = sys.stdin.read(1)
+ if c2 not in "\x4f\x5b": # O[
+ return c1 + c2
+
+ c3 = sys.stdin.read(1)
+ if c3 not in "\x31\x32\x33\x35\x36": # 12356
+ return c1 + c2 + c3
+
+ c4 = sys.stdin.read(1)
+ if c4 not in "\x30\x31\x33\x34\x35\x37\x38\x39": # 01345789
+ return c1 + c2 + c3 + c4
+
+ c5 = sys.stdin.read(1)
+ key = c1 + c2 + c3 + c4 + c5
+
+ # Bracketed Paste Mode: # https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-Bracketed-Paste-Mode
+ if key == START_PASTE[:-1] or key == END_PASTE[:-1]:
+ c6 = sys.stdin.read(1)
+ return key + c6
+
+ return key
+
+
+def windows_readkey() -> str:
+ """Reads the next keypress. If an escaped key is pressed, the full
+ sequence is read and returned.
+
+ Copied from readchar:
+ https://github.com/magmax/python-readchar/blob/master/readchar/_win_read.py#LL14C1-L30C24
+ """
+
+ ch = sys.stdin.read(1)
+
+ # if it is a normal character:
+ if ch not in "\x00\xe0":
+ return ch
+
+ # if it is a scpeal key, read second half:
+ ch2 = sys.stdin.read(1)
+
+ return "\x00" + ch2
+
+
+def posix_shell(chan: "paramiko.Channel"): # noqa: C901
+ import select
+
+ oldtty = termios.tcgetattr(sys.stdin)
+
+ try:
+ tty.setraw(sys.stdin.fileno())
+ tty.setcbreak(sys.stdin.fileno())
+ chan.settimeout(0.0)
+ while True:
+ r, w, e = select.select([chan, sys.stdin], [], [])
+ if chan in r:
+ try:
+ x = chan.recv(1024).decode()
+ if len(x) == 0:
+ sys.stdout.write("\r\n")
+ break
+ sys.stdout.write(x)
+ sys.stdout.flush()
+ except socket.timeout:
+ pass
+ if sys.stdin in r:
+ key = posix_readkey()
+ # When pasting something, we need to read the entire pasted blob at once
+ # Otherwise it'll hang until the next key press.
+ # This has to do with how 'select.select' detects changes.
+ # A paste is a single event of many characters, so we must handle them all as one event
+ if key == START_PASTE:
+ # Start reading the pasted text
+ key = posix_readkey()
+ # Until we reach the end of the pasted text
+ while key != END_PASTE:
+ chan.send(key)
+ key = posix_readkey()
+ # We've exhausted the paste event, wait for next event
+ continue
+
+ if len(key) == 0:
+ break
+ chan.send(key)
+
+ finally:
+ termios.tcsetattr(sys.stdin, termios.TCSADRAIN, oldtty)
+
+
+# thanks to Mike Looijmans for this code
+def windows_shell(chan: "paramiko.Channel"):
+ import threading
+
+ sys.stdout.write("Line-buffered terminal emulation. Press F6 or ^Z to send EOF.\r\n\r\n")
+
+ def writeall(sock):
+ while True:
+ data = sock.recv(256)
+ if not data:
+ sys.stdout.write("\r\n*** EOF ***\r\n\r\n")
+ sys.stdout.flush()
+ break
+ sys.stdout.write(data)
+ sys.stdout.flush()
+
+ writer = threading.Thread(target=writeall, args=(chan,))
+ writer.start()
+
+ try:
+ while True:
+ d = windows_readkey()
+ if not d:
+ break
+ chan.send(d)
+ except EOFError:
+ # user hit ^Z or F6
+ pass
diff --git a/sdk/src/beta9/cli/main.py b/sdk/src/beta9/cli/main.py
index 8a7168825..a553eadb2 100644
--- a/sdk/src/beta9/cli/main.py
+++ b/sdk/src/beta9/cli/main.py
@@ -16,6 +16,7 @@
pool,
secret,
serve,
+ shell,
task,
token,
volume,
@@ -100,6 +101,7 @@ def load_cli(check_config=True, **kwargs: Any) -> CLI:
cli.register(secret)
cli.register(token)
cli.register(worker)
+ cli.register(shell)
if check_config:
cli.check_config()
diff --git a/sdk/src/beta9/cli/shell.py b/sdk/src/beta9/cli/shell.py
new file mode 100644
index 000000000..f0e7d5893
--- /dev/null
+++ b/sdk/src/beta9/cli/shell.py
@@ -0,0 +1,79 @@
+import importlib
+import os
+import sys
+from pathlib import Path
+
+import click
+
+from .. import terminal
+from ..channel import ServiceClient
+from ..cli import extraclick
+from .extraclick import ClickCommonGroup
+
+
+@click.group(cls=ClickCommonGroup)
+def common(**_):
+ pass
+
+
+@common.command(
+ name="shell",
+ help="""
+ Connect to a container with the same config as your handler.
+
+ ENTRYPOINT is in the format of "file:function".
+ """,
+ epilog="""
+ Examples:
+
+ {cli_name} shell app.py:handler
+
+ {cli_name} shell app.py:my_func
+ \b
+ """,
+)
+@click.argument(
+ "entrypoint",
+ nargs=1,
+ required=True,
+)
+@click.option(
+ "--url-type",
+ help="The type of URL to get back. [default is determined by the server] ",
+ type=click.Choice(["host", "path"]),
+)
+@extraclick.pass_service_client
+@click.pass_context
+def shell(
+ ctx: click.Context,
+ service: ServiceClient,
+ entrypoint: str,
+ url_type: str = "path",
+):
+ current_dir = os.getcwd()
+ if current_dir not in sys.path:
+ sys.path.insert(0, current_dir)
+
+ module_path, obj_name, *_ = entrypoint.split(":") if ":" in entrypoint else (entrypoint, "")
+ module_name = module_path.replace(".py", "").replace(os.path.sep, ".")
+
+ if not Path(module_path).exists():
+ terminal.error(f"Unable to find file: '{module_path}'")
+
+ if not obj_name:
+ terminal.error(
+ "Invalid handler function specified. Expected format: beam shell [file.py]:[function]"
+ )
+
+ module = importlib.import_module(module_name)
+
+ user_obj = getattr(module, obj_name, None)
+ if user_obj is None:
+ terminal.error(
+ f"Invalid handler function specified. Make sure '{module_path}' contains the function: '{obj_name}'"
+ )
+
+ if hasattr(user_obj, "set_handler"):
+ user_obj.set_handler(f"{module_name}:{obj_name}")
+
+ user_obj.shell(url_type=url_type) # type:ignore
diff --git a/sdk/src/beta9/clients/shell/__init__.py b/sdk/src/beta9/clients/shell/__init__.py
new file mode 100644
index 000000000..000ea4476
--- /dev/null
+++ b/sdk/src/beta9/clients/shell/__init__.py
@@ -0,0 +1,46 @@
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# sources: shell.proto
+# plugin: python-betterproto
+# This file has been @generated
+
+from dataclasses import dataclass
+from typing import (
+ TYPE_CHECKING,
+ Dict,
+ Optional,
+)
+
+import betterproto
+import grpc
+from betterproto.grpcstub.grpcio_client import SyncServiceStub
+from betterproto.grpcstub.grpclib_server import ServiceBase
+
+
+if TYPE_CHECKING:
+ import grpclib.server
+ from betterproto.grpcstub.grpclib_client import MetadataLike
+ from grpclib.metadata import Deadline
+
+
+@dataclass(eq=False, repr=False)
+class CreateShellRequest(betterproto.Message):
+ stub_id: str = betterproto.string_field(1)
+
+
+@dataclass(eq=False, repr=False)
+class CreateShellResponse(betterproto.Message):
+ ok: bool = betterproto.bool_field(1)
+ container_id: str = betterproto.string_field(2)
+ token: str = betterproto.string_field(3)
+ err_msg: str = betterproto.string_field(4)
+
+
+class ShellServiceStub(SyncServiceStub):
+ def create_shell(
+ self, create_shell_request: "CreateShellRequest"
+ ) -> "CreateShellResponse":
+ return self._unary_unary(
+ "/shell.ShellService/CreateShell",
+ CreateShellRequest,
+ CreateShellResponse,
+ )(create_shell_request)