diff --git a/.dockerignore b/.dockerignore index 49585d79..cbc2549e 100644 --- a/.dockerignore +++ b/.dockerignore @@ -19,9 +19,14 @@ dist # Infra (not needed in build) infra/ +docs/ +proto/ # Apps not needed in builds -clients/ios/ +clients/ + +# Services not needed for root-context agent build +services/control-plane/ # Scripts scripts/ diff --git a/.env.example b/.env.example index 29c35b23..3feff490 100644 --- a/.env.example +++ b/.env.example @@ -1,8 +1,16 @@ # Netclode Environment Variables # Copy to .env and fill in values -# Anthropic API Key (required) -ANTHROPIC_API_KEY=sk-ant-... +# LLM credentials (required for provider SDKs; Codex OAuth sessions use local `netclode auth codex`) +ANTHROPIC_API_KEY= +OPENAI_API_KEY= +MISTRAL_API_KEY= +OPENCODE_API_KEY= +ZAI_API_KEY= + +# Codex OAuth session storage encryption key (required only if using Codex :oauth sessions) +# Generate with: openssl rand -base64 32 +CODEX_OAUTH_ENCRYPTION_KEY_B64= # Tailscale OAuth for Kubernetes operator (required) # Create at: https://login.tailscale.com/admin/settings/oauth @@ -15,12 +23,21 @@ TS_OAUTH_CLIENT_SECRET= # If not set, authenticate manually on the host: tailscale up --ssh TAILSCALE_AUTHKEY= -# JuiceFS S3 backend (DigitalOcean Spaces) +# JuiceFS S3 backend (external S3 provider) +# Optional when MINIO_ENABLED=true (auto-wired from /var/secrets/minio-root-*) DO_SPACES_ACCESS_KEY= DO_SPACES_SECRET_KEY= JUICEFS_BUCKET=https://nyc3.digitaloceanspaces.com/your-bucket JUICEFS_META_URL=redis://localhost:6379/0 +# Self-hosted MinIO (optional) +# Set MINIO_ENABLED=true, run ansible playbook with --tags minio +# MinIO can reuse DO_SPACES_ACCESS_KEY / DO_SPACES_SECRET_KEY as credentials +MINIO_ENABLED=false +# MINIO_BUCKET_NAME=netclode-juicefs +# MINIO_API_PORT=9000 +# MINIO_CONSOLE_PORT=9001 + # Deployment target (for deploy-secrets.sh, rollout.sh) DEPLOY_HOST=root@your-server diff --git a/AGENTS.md b/AGENTS.md index ab1d37c7..074f8e2b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -5,6 +5,7 @@ - Always ask before running `git push` - Prefer breaking changes over backwards compatibility (no `reserved` fields in protos, etc.) - **Never make manual changes to servers** - always use Ansible. If debugging requires manual changes, backport them to Ansible immediately. +- When making changes in any `clients/*` project, read that client's local `README.md` first (for example, `clients/ios/README.md` or `clients/cli/README.md`). ## Deployment diff --git a/Makefile b/Makefile index 00282202..8cbe897c 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,31 @@ CONTEXT ?= netclode NAMESPACE ?= netclode +TAG ?= dev-$(shell date +%Y%m%d-%H%M%S) +DEV_CONTROL_PLANE_IMAGE ?= netclode-control-plane:$(TAG) +DEV_AGENT_IMAGE ?= netclode-agent:$(TAG) +ANSIBLE_EXTRA_ARGS ?= +ANSIBLE_USER ?= -.PHONY: rollout rollout-control-plane rollout-agent deploy test-ios run-macos run-ios run-device proto proto-lint proto-breaking proto-setup +TEAM_ID_AUTO ?= $(shell \ + team=$$(security find-certificate -a -c "Apple Development" -p "$$HOME/Library/Keychains/login.keychain-db" 2>/dev/null | \ + openssl x509 -noout -subject 2>/dev/null | sed -n 's/.*OU=\([^,]*\).*/\1/p' | head -n 1); \ + if [ -z "$$team" ]; then \ + profile=$$(ls -1 "$$HOME/Library/Developer/Xcode/UserData/Provisioning Profiles"/*.mobileprovision 2>/dev/null | head -n 1); \ + if [ -n "$$profile" ]; then \ + team=$$(security cms -D -i "$$profile" 2>/dev/null | \ + awk 'found && //{gsub(/.*|<\/string>.*/, ""); print; exit} /TeamIdentifier<\/key>/{found=1}'); \ + fi; \ + fi; \ + printf "%s" "$$team" \ +) +TEAM_ID ?= $(TEAM_ID_AUTO) + +XCODE_SIGN_ARGS := -allowProvisioningUpdates +ifneq ($(strip $(TEAM_ID)),) +XCODE_SIGN_ARGS += DEVELOPMENT_TEAM=$(TEAM_ID) +endif + +.PHONY: rollout rollout-control-plane rollout-agent drain-warmpool deploy test-ios run-macos run-ios run-device print-ios-team-id proto proto-lint proto-breaking proto-setup dev-install-builder dev-build-remote dev-build-remote-control-plane dev-build-remote-agent dev-deploy-images dev-verify dev-loop-remote dev-loop-ansible dev-loop-ansible-build dev-loop-ansible-deploy dev-loop-ansible-verify # Proto generation proto: proto-setup ## Generate code from proto files @@ -50,24 +74,89 @@ deploy: ## Wait for CI then rollout control-plane gh run watch $$(gh run list --limit 1 --json databaseId --jq '.[0].databaseId') --exit-status $(MAKE) rollout-control-plane +dev-build-remote: ## Build control-plane + agent images on DEPLOY_HOST and import into k3s containerd + @TAG=$(TAG) CONTROL_PLANE_IMAGE=$(DEV_CONTROL_PLANE_IMAGE) AGENT_IMAGE=$(DEV_AGENT_IMAGE) scripts/dev/build-on-remote.sh + +dev-build-remote-control-plane: ## Build only control-plane image on DEPLOY_HOST and import into k3s containerd + @TAG=$(TAG) BUILD_AGENT=0 CONTROL_PLANE_IMAGE=$(DEV_CONTROL_PLANE_IMAGE) scripts/dev/build-on-remote.sh + +dev-build-remote-agent: ## Build only agent image on DEPLOY_HOST and import into k3s containerd + @TAG=$(TAG) BUILD_CONTROL_PLANE=0 AGENT_IMAGE=$(DEV_AGENT_IMAGE) scripts/dev/build-on-remote.sh + +dev-deploy-images: ## Fast dev deploy via kubectl (no Ansible): update images + AGENT_IMAGE env + refresh warm pool + @CONTROL_PLANE_IMAGE=$(DEV_CONTROL_PLANE_IMAGE) AGENT_IMAGE=$(DEV_AGENT_IMAGE) CONTEXT=$(CONTEXT) NAMESPACE=$(NAMESPACE) scripts/dev/deploy-dev-images.sh + +dev-verify: ## Verify control-plane + sandbox template image wiring and logs + @CONTEXT=$(CONTEXT) NAMESPACE=$(NAMESPACE) scripts/dev/verify-dev-loop.sh + +dev-loop-remote: ## Build on DEPLOY_HOST, deploy with kubectl patch, then verify (TAG=dev-...) + @$(MAKE) dev-build-remote TAG=$(TAG) + @$(MAKE) dev-deploy-images TAG=$(TAG) + @$(MAKE) dev-verify + +dev-install-builder: ## Install Docker + Buildx on DEPLOY_HOST for remote dev builds + @set -a; [ -f .env ] && . ./.env || true; set +a; \ + cd infra/ansible && ansible-playbook playbooks/dev-builder.yaml \ + $(if $(strip $(ANSIBLE_USER)),-e ansible_user=$(ANSIBLE_USER),) \ + $(ANSIBLE_EXTRA_ARGS) + +dev-loop-ansible: ## Fast dev loop via Ansible (build + deploy + verify) + @set -a; [ -f .env ] && . ./.env || true; set +a; \ + cd infra/ansible && ansible-playbook playbooks/dev-loop.yaml \ + $(if $(strip $(ANSIBLE_USER)),-e ansible_user=$(ANSIBLE_USER),) \ + -e k8s_namespace=$(NAMESPACE) \ + -e tag=$(TAG) \ + -e control_plane_image=$(DEV_CONTROL_PLANE_IMAGE) \ + -e agent_image=$(DEV_AGENT_IMAGE) \ + $(ANSIBLE_EXTRA_ARGS) + +dev-loop-ansible-build: ## Ansible dev loop: build/import only + @set -a; [ -f .env ] && . ./.env || true; set +a; \ + cd infra/ansible && ansible-playbook playbooks/dev-loop.yaml --tags dev-build \ + $(if $(strip $(ANSIBLE_USER)),-e ansible_user=$(ANSIBLE_USER),) \ + -e k8s_namespace=$(NAMESPACE) \ + -e tag=$(TAG) \ + -e control_plane_image=$(DEV_CONTROL_PLANE_IMAGE) \ + -e agent_image=$(DEV_AGENT_IMAGE) \ + $(ANSIBLE_EXTRA_ARGS) + +dev-loop-ansible-deploy: ## Ansible dev loop: deploy/rollout only + @set -a; [ -f .env ] && . ./.env || true; set +a; \ + cd infra/ansible && ansible-playbook playbooks/dev-loop.yaml --tags dev-deploy \ + $(if $(strip $(ANSIBLE_USER)),-e ansible_user=$(ANSIBLE_USER),) \ + -e k8s_namespace=$(NAMESPACE) \ + -e control_plane_image=$(DEV_CONTROL_PLANE_IMAGE) \ + -e agent_image=$(DEV_AGENT_IMAGE) \ + $(ANSIBLE_EXTRA_ARGS) + +dev-loop-ansible-verify: ## Ansible dev loop: verification only + @set -a; [ -f .env ] && . ./.env || true; set +a; \ + cd infra/ansible && ansible-playbook playbooks/dev-loop.yaml --tags dev-verify \ + $(if $(strip $(ANSIBLE_USER)),-e ansible_user=$(ANSIBLE_USER),) \ + -e k8s_namespace=$(NAMESPACE) \ + $(ANSIBLE_EXTRA_ARGS) + test-ios: ## Run iOS unit tests cd clients/ios && xcodebuild test -scheme NetclodeTests -destination 'platform=macOS' -quiet run-macos: ## Build and run macOS (Catalyst) app - cd clients/ios && xcodebuild -scheme Netclode -destination 'platform=macOS,variant=Mac Catalyst' -derivedDataPath .build build + cd clients/ios && xcodebuild -scheme Netclode -destination 'platform=macOS,variant=Mac Catalyst' -derivedDataPath .build $(XCODE_SIGN_ARGS) build open clients/ios/.build/Build/Products/Debug-maccatalyst/Netclode.app SIMULATOR ?= iPhone 16 Pro run-ios: ## Build and run iOS simulator app (SIMULATOR="iPhone 16 Pro") xcrun simctl boot "$(SIMULATOR)" 2>/dev/null || true - cd clients/ios && xcodebuild -scheme Netclode -destination 'platform=iOS Simulator,name=$(SIMULATOR)' -derivedDataPath .build build + cd clients/ios && xcodebuild -scheme Netclode -destination 'platform=iOS Simulator,name=$(SIMULATOR)' -derivedDataPath .build $(XCODE_SIGN_ARGS) build xcrun simctl install "$(SIMULATOR)" clients/ios/.build/Build/Products/Debug-iphonesimulator/Netclode.app xcrun simctl launch "$(SIMULATOR)" com.netclode.ios run-device: ## Build and run on connected iPhone - cd clients/ios && xcodebuild -scheme Netclode -destination 'generic/platform=iOS' -derivedDataPath .build build + cd clients/ios && xcodebuild -scheme Netclode -destination 'generic/platform=iOS' -derivedDataPath .build $(XCODE_SIGN_ARGS) -allowProvisioningDeviceRegistration build xcrun devicectl device install app --device "$(shell xcrun devicectl list devices 2>/dev/null | grep iPhone | grep -oE '[0-9A-F-]{36}' | head -1)" clients/ios/.build/Build/Products/Debug-iphoneos/Netclode.app xcrun devicectl device process launch --device "$(shell xcrun devicectl list devices 2>/dev/null | grep iPhone | grep -oE '[0-9A-F-]{36}' | head -1)" com.netclode.ios +print-ios-team-id: ## Print detected iOS signing Team ID (override with TEAM_ID=...) + @echo $(TEAM_ID) + help: ## Show this help @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}' diff --git a/README.md b/README.md index a87df1d7..be4ca99f 100644 --- a/README.md +++ b/README.md @@ -120,6 +120,7 @@ Quick version: ## Docs - [Deployment](docs/deployment.md) - Full setup +- [Fast Dev Iteration](docs/dev-iteration.md) - Remote build + fast rollout loop - [Operations](docs/operations.md) - Day-to-day management - [Sandbox Architecture](docs/sandbox-architecture.md) - Kata VMs, JuiceFS, warm pool - [Session Lifecycle](docs/session-lifecycle.md) - How sessions work diff --git a/clients/cli/README.md b/clients/cli/README.md index 5e705dbd..881e869d 100644 --- a/clients/cli/README.md +++ b/clients/cli/README.md @@ -89,8 +89,8 @@ netclode sessions create --repo owner/repo --repo owner/other --name "Multi Repo # With SDK type (claude, opencode, copilot, codex) netclode sessions create --repo owner/repo --sdk opencode --model anthropic/claude-sonnet-4-0 -# With Codex SDK -netclode sessions create --repo owner/repo --sdk codex --model codex-mini-latest +# With Codex SDK (auth suffix required: :oauth or :api) +netclode sessions create --repo owner/repo --sdk codex --model gpt-5-codex:oauth:high # With Tailnet access netclode sessions create --repo owner/repo --tailnet # Allow Tailnet access (100.64.0.0/10) @@ -270,7 +270,7 @@ netclode auth codex This will: 1. Display a verification URL and code 2. Wait for you to authorize in your browser -3. Output tokens to add to your `.env` file +3. Store OAuth tokens locally for CLI Codex `:oauth` sessions Example output: ``` @@ -281,13 +281,19 @@ Waiting for authorization... Authentication successful! -Add these to your .env file: ------------------------------ -CODEX_ACCESS_TOKEN=eyJ... -CODEX_REFRESH_TOKEN=... -CODEX_ID_TOKEN=eyJ... +Stored local Codex OAuth tokens for CLI session creation. +``` + +Check current auth status: -Then deploy with: cd infra/ansible && DEPLOY_HOST= ansible-playbook playbooks/site.yaml +```bash +netclode auth codex status +``` + +Remove local OAuth tokens: + +```bash +netclode auth codex logout ``` ## Global Flags @@ -352,7 +358,8 @@ clients/cli/ │ ├── client.go # Connect protocol client │ └── client_test.go # Client tests ├── codex/ - │ └── oauth.go # Codex OAuth device code flow + │ ├── oauth.go # Codex OAuth device code flow + │ └── store.go # Local Codex OAuth token storage/refresh └── output/ ├── format.go # Output formatting └── format_test.go # Formatting tests diff --git a/clients/cli/cmd/auth.go b/clients/cli/cmd/auth.go index f629894b..cff988d4 100644 --- a/clients/cli/cmd/auth.go +++ b/clients/cli/cmd/auth.go @@ -1,10 +1,12 @@ package cmd import ( + "context" "fmt" "time" - "github.com/angristan/netclode/clients/cli/internal/codex" + "github.com/angristan/netclode/clients/cli/internal/client" + pb "github.com/angristan/netclode/services/control-plane/gen/netclode/v1" "github.com/spf13/cobra" ) @@ -22,59 +24,128 @@ var authCodexCmd = &cobra.Command{ This command will: 1. Display a verification URL and code 2. Wait for you to authorize in your browser -3. Output tokens to add to your .env file - -The tokens are then deployed to production via Ansible.`, +3. Complete authentication on the backend`, RunE: runAuthCodex, } +var authCodexStatusCmd = &cobra.Command{ + Use: "status", + Short: "Show backend Codex OAuth status", + RunE: runAuthCodexStatus, +} + +var authCodexLogoutCmd = &cobra.Command{ + Use: "logout", + Short: "Delete backend Codex OAuth tokens", + RunE: runAuthCodexLogout, +} + func init() { rootCmd.AddCommand(authCmd) authCmd.AddCommand(authCodexCmd) + authCodexCmd.AddCommand(authCodexStatusCmd) + authCodexCmd.AddCommand(authCodexLogoutCmd) } func runAuthCodex(cmd *cobra.Command, args []string) error { + ctx := context.Background() + c := client.New(getServerURL()) + fmt.Println("Codex Authentication (ChatGPT OAuth)") fmt.Println("=====================================") fmt.Println() - // Step 1: Request device code fmt.Println("Requesting device code...") - dc, err := codex.RequestDeviceCode() + started, err := c.CodexAuthStart(ctx) if err != nil { - return fmt.Errorf("failed to request device code: %w", err) + return fmt.Errorf("failed to start backend codex auth: %w", err) } fmt.Println() - fmt.Printf("Visit: %s\n", dc.VerificationURL) - fmt.Printf("Code: %s\n", dc.UserCode) + fmt.Printf("Visit: %s\n", started.VerificationUri) + fmt.Printf("Code: %s\n", started.UserCode) fmt.Println() - fmt.Println("Waiting for authorization (15 minute timeout)...") + fmt.Println("Waiting for backend authentication to complete...") - // Step 2: Poll for authorization - ce, err := codex.PollForAuthorization(dc, 15*time.Minute) - if err != nil { - return fmt.Errorf("authorization failed: %w", err) + interval := time.Duration(started.IntervalSeconds) * time.Second + if interval <= 0 { + interval = 5 * time.Second + } + deadline := time.Now().Add(15 * time.Minute) + if started.ExpiresAt != nil { + deadline = started.ExpiresAt.AsTime() } - fmt.Println("Authorization received, exchanging for tokens...") + for time.Now().Before(deadline) { + status, err := c.CodexAuthStatus(ctx) + if err != nil { + return fmt.Errorf("failed to check auth status: %w", err) + } + + switch status.State { + case pb.CodexAuthState_CODEX_AUTH_STATE_READY: + fmt.Println() + fmt.Println("Authentication successful!") + if status.AccountId != nil && *status.AccountId != "" { + fmt.Printf("Account: %s\n", *status.AccountId) + } + if status.ExpiresAt != nil { + fmt.Printf("Token expires at: %s\n", status.ExpiresAt.AsTime().Format(time.RFC3339)) + } + return nil + case pb.CodexAuthState_CODEX_AUTH_STATE_ERROR: + if status.Error != nil { + return fmt.Errorf("authentication failed: %s", *status.Error) + } + return fmt.Errorf("authentication failed") + } + + time.Sleep(interval) + } - // Step 3: Exchange for tokens - tokens, err := codex.ExchangeCodeForTokens(ce) + return fmt.Errorf("authentication timed out") +} + +func runAuthCodexStatus(cmd *cobra.Command, args []string) error { + ctx := context.Background() + c := client.New(getServerURL()) + + status, err := c.CodexAuthStatus(ctx) if err != nil { - return fmt.Errorf("token exchange failed: %w", err) + return fmt.Errorf("failed to read codex oauth status: %w", err) } - fmt.Println() - fmt.Println("Authentication successful!") - fmt.Println() - fmt.Println("Add these to your .env file:") - fmt.Println("-----------------------------") - fmt.Printf("CODEX_ACCESS_TOKEN=%s\n", tokens.AccessToken) - fmt.Printf("CODEX_REFRESH_TOKEN=%s\n", tokens.RefreshToken) - fmt.Printf("CODEX_ID_TOKEN=%s\n", tokens.IDToken) - fmt.Println() - fmt.Println("Then deploy with: cd infra/ansible && DEPLOY_HOST= ansible-playbook playbooks/site.yaml") + switch status.State { + case pb.CodexAuthState_CODEX_AUTH_STATE_READY: + fmt.Println("Codex OAuth: authenticated") + if status.AccountId != nil && *status.AccountId != "" { + fmt.Printf("Account: %s\n", *status.AccountId) + } + if status.ExpiresAt != nil { + fmt.Printf("Expires at: %s\n", status.ExpiresAt.AsTime().Format(time.RFC3339)) + } + case pb.CodexAuthState_CODEX_AUTH_STATE_PENDING: + fmt.Println("Codex OAuth: pending authorization") + if status.ExpiresAt != nil { + fmt.Printf("Pending expires at: %s\n", status.ExpiresAt.AsTime().Format(time.RFC3339)) + } + case pb.CodexAuthState_CODEX_AUTH_STATE_ERROR: + fmt.Println("Codex OAuth: error") + if status.Error != nil && *status.Error != "" { + fmt.Printf("Error: %s\n", *status.Error) + } + default: + fmt.Println("Codex OAuth: not authenticated") + } + return nil +} +func runAuthCodexLogout(cmd *cobra.Command, args []string) error { + ctx := context.Background() + c := client.New(getServerURL()) + if err := c.CodexAuthLogout(ctx); err != nil { + return fmt.Errorf("failed to delete backend codex oauth tokens: %w", err) + } + fmt.Println("Codex OAuth tokens removed from backend.") return nil } diff --git a/clients/cli/cmd/events.go b/clients/cli/cmd/events.go index 9ccaed7f..ca89c89f 100644 --- a/clients/cli/cmd/events.go +++ b/clients/cli/cmd/events.go @@ -235,6 +235,11 @@ func getEventDetails(e *pb.AgentEvent) string { return fmt.Sprintf("port=%d%s", port.Port, url) } + case pb.AgentEventKind_AGENT_EVENT_KIND_PORT_UNEXPOSED: + if port := e.GetPortUnexposed(); port != nil { + return fmt.Sprintf("port=%d removed", port.Port) + } + case pb.AgentEventKind_AGENT_EVENT_KIND_REPO_CLONE: if repo := e.GetRepoClone(); repo != nil { stage := repo.Stage.String() diff --git a/clients/cli/cmd/port.go b/clients/cli/cmd/port.go index 83138d5c..eaf083bc 100644 --- a/clients/cli/cmd/port.go +++ b/clients/cli/cmd/port.go @@ -23,8 +23,16 @@ var portExposeCmd = &cobra.Command{ RunE: runPortExpose, } +var portUnexposeCmd = &cobra.Command{ + Use: "unexpose ", + Short: "Remove an exposed port for a session", + Args: cobra.ExactArgs(2), + RunE: runPortUnexpose, +} + func init() { portCmd.AddCommand(portExposeCmd) + portCmd.AddCommand(portUnexposeCmd) rootCmd.AddCommand(portCmd) } @@ -96,3 +104,71 @@ func runPortExpose(cmd *cobra.Command, args []string) error { } } } + +func runPortUnexpose(cmd *cobra.Command, args []string) error { + sessionID := args[0] + port, err := strconv.Atoi(args[1]) + if err != nil { + return fmt.Errorf("invalid port: %w", err) + } + + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + c := client.New(getServerURL()) + + stream := c.Stream(ctx) + defer func() { _ = stream.CloseRequest() }() + + // Open session first + if err := stream.Send(&pb.ClientMessage{ + Message: &pb.ClientMessage_OpenSession{ + OpenSession: &pb.OpenSessionRequest{ + SessionId: sessionID, + }, + }, + }); err != nil { + return fmt.Errorf("open session: %w", err) + } + + // Wait for session state + msg, err := stream.Receive() + if err != nil { + return fmt.Errorf("receive session state: %w", err) + } + if errResp := msg.GetError(); errResp != nil { + return fmt.Errorf("%s: %s", errResp.Error.Code, errResp.Error.Message) + } + if msg.GetSessionState() == nil { + return fmt.Errorf("expected session state, got %T", msg.GetMessage()) + } + + // Send unexpose port request + if err := stream.Send(&pb.ClientMessage{ + Message: &pb.ClientMessage_UnexposePort{ + UnexposePort: &pb.UnexposePortRequest{ + SessionId: sessionID, + Port: int32(port), + }, + }, + }); err != nil { + return fmt.Errorf("send unexpose port: %w", err) + } + + // Wait for response + for { + msg, err := stream.Receive() + if err != nil { + return fmt.Errorf("receive: %w", err) + } + + if resp := msg.GetPortUnexposed(); resp != nil { + fmt.Printf("Port %d unexposed\n", port) + return nil + } + + if errResp := msg.GetError(); errResp != nil { + return fmt.Errorf("error: %s: %s", errResp.Error.Code, errResp.Error.Message) + } + } +} diff --git a/clients/cli/cmd/sessions.go b/clients/cli/cmd/sessions.go index b043590f..53551e9f 100644 --- a/clients/cli/cmd/sessions.go +++ b/clients/cli/cmd/sessions.go @@ -329,6 +329,13 @@ func runSessionsCreate(cmd *cobra.Command, args []string) error { MemoryMB: createMemoryMB, } + if sdkType == pb.SdkType_SDK_TYPE_CODEX { + authMode := codexModelAuthMode(createModel) + if authMode == "" { + return fmt.Errorf("codex model must include auth suffix (:oauth or :api), e.g. gpt-5-codex:oauth:high") + } + } + session, err := c.CreateSession(ctx, opts) if err != nil { return fmt.Errorf("create session: %w", err) @@ -357,6 +364,8 @@ func formatSdkType(sdkType pb.SdkType) string { return "opencode" case pb.SdkType_SDK_TYPE_COPILOT: return "copilot" + case pb.SdkType_SDK_TYPE_CODEX: + return "codex" case pb.SdkType_SDK_TYPE_CLAUDE: return "claude" default: @@ -364,6 +373,25 @@ func formatSdkType(sdkType pb.SdkType) string { } } +func codexModelAuthMode(model string) string { + parts := strings.Split(model, ":") + if len(parts) < 2 { + return "" + } + last := parts[len(parts)-1] + switch last { + case "minimal", "low", "medium", "high", "xhigh": + if len(parts) < 3 { + return "" + } + last = parts[len(parts)-2] + } + if last == "api" || last == "oauth" { + return last + } + return "" +} + func runSessionsPause(cmd *cobra.Command, args []string) error { ctx := context.Background() c := client.New(getServerURL()) diff --git a/clients/cli/internal/client/client.go b/clients/cli/internal/client/client.go index 541c88bb..e6d3334b 100644 --- a/clients/cli/internal/client/client.go +++ b/clients/cli/internal/client/client.go @@ -67,7 +67,6 @@ func (c *Client) CreateSession(ctx context.Context, opts CreateSessionOptions) ( MemoryMb: opts.MemoryMB, } } - if err := stream.Send(&pb.ClientMessage{ Message: &pb.ClientMessage_CreateSession{ CreateSession: req, @@ -410,6 +409,81 @@ func (c *Client) ListModels(ctx context.Context, sdkType pb.SdkType, copilotBack return nil, fmt.Errorf("unexpected response type: %T", msg.GetMessage()) } +func (c *Client) CodexAuthStart(ctx context.Context) (*pb.CodexAuthStartedResponse, error) { + stream := c.client.Connect(ctx) + defer func() { _ = stream.CloseRequest() }() + + if err := stream.Send(&pb.ClientMessage{ + Message: &pb.ClientMessage_CodexAuthStart{ + CodexAuthStart: &pb.CodexAuthStartRequest{}, + }, + }); err != nil { + return nil, fmt.Errorf("send request: %w", err) + } + + msg, err := stream.Receive() + if err != nil { + return nil, fmt.Errorf("receive response: %w", err) + } + if resp := msg.GetCodexAuthStarted(); resp != nil { + return resp, nil + } + if errResp := msg.GetError(); errResp != nil { + return nil, fmt.Errorf("%s: %s", errResp.Error.Code, errResp.Error.Message) + } + return nil, fmt.Errorf("unexpected response type: %T", msg.GetMessage()) +} + +func (c *Client) CodexAuthStatus(ctx context.Context) (*pb.CodexAuthStatusResponse, error) { + stream := c.client.Connect(ctx) + defer func() { _ = stream.CloseRequest() }() + + if err := stream.Send(&pb.ClientMessage{ + Message: &pb.ClientMessage_CodexAuthStatus{ + CodexAuthStatus: &pb.CodexAuthStatusRequest{}, + }, + }); err != nil { + return nil, fmt.Errorf("send request: %w", err) + } + + msg, err := stream.Receive() + if err != nil { + return nil, fmt.Errorf("receive response: %w", err) + } + if resp := msg.GetCodexAuthStatus(); resp != nil { + return resp, nil + } + if errResp := msg.GetError(); errResp != nil { + return nil, fmt.Errorf("%s: %s", errResp.Error.Code, errResp.Error.Message) + } + return nil, fmt.Errorf("unexpected response type: %T", msg.GetMessage()) +} + +func (c *Client) CodexAuthLogout(ctx context.Context) error { + stream := c.client.Connect(ctx) + defer func() { _ = stream.CloseRequest() }() + + if err := stream.Send(&pb.ClientMessage{ + Message: &pb.ClientMessage_CodexAuthLogout{ + CodexAuthLogout: &pb.CodexAuthLogoutRequest{}, + }, + }); err != nil { + return fmt.Errorf("send request: %w", err) + } + + msg, err := stream.Receive() + if err != nil { + return fmt.Errorf("receive response: %w", err) + } + if msg.GetCodexAuthLoggedOut() != nil { + return nil + } + if errResp := msg.GetError(); errResp != nil { + return fmt.Errorf("%s: %s", errResp.Error.Code, errResp.Error.Message) + } + return fmt.Errorf("unexpected response type: %T", msg.GetMessage()) +} + // RestoreSnapshot restores a session to a snapshot. func (c *Client) RestoreSnapshot(ctx context.Context, sessionID, snapshotID string) error { stream := c.client.Connect(ctx) diff --git a/clients/cli/internal/codex/oauth.go b/clients/cli/internal/codex/oauth.go index 964116cd..53a8f63f 100644 --- a/clients/cli/internal/codex/oauth.go +++ b/clients/cli/internal/codex/oauth.go @@ -33,6 +33,7 @@ type Tokens struct { AccessToken string `json:"access_token"` RefreshToken string `json:"refresh_token"` IDToken string `json:"id_token"` + ExpiresIn int64 `json:"expires_in,omitempty"` } // RequestDeviceCode requests a device code from OpenAI auth diff --git a/clients/cli/internal/codex/store.go b/clients/cli/internal/codex/store.go new file mode 100644 index 00000000..b39c63fd --- /dev/null +++ b/clients/cli/internal/codex/store.go @@ -0,0 +1,176 @@ +package codex + +import ( + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "os" + "path/filepath" + "strings" + "time" +) + +const ( + storedTokensFilename = "codex-auth.json" +) + +// StoredTokens represents locally cached Codex OAuth tokens. +type StoredTokens struct { + AccessToken string `json:"access_token"` + RefreshToken string `json:"refresh_token"` + IDToken string `json:"id_token"` + ExpiresAt *time.Time `json:"expires_at,omitempty"` + UpdatedAt time.Time `json:"updated_at"` +} + +func storedTokensPath() (string, error) { + configDir, err := os.UserConfigDir() + if err != nil { + return "", err + } + return filepath.Join(configDir, "netclode", storedTokensFilename), nil +} + +func LoadStoredTokens() (*StoredTokens, error) { + path, err := storedTokensPath() + if err != nil { + return nil, err + } + raw, err := os.ReadFile(path) + if err != nil { + if errors.Is(err, os.ErrNotExist) { + return nil, nil + } + return nil, err + } + var tokens StoredTokens + if err := json.Unmarshal(raw, &tokens); err != nil { + return nil, err + } + return &tokens, nil +} + +func SaveStoredTokens(tokens *StoredTokens) error { + if tokens == nil { + return fmt.Errorf("tokens are required") + } + path, err := storedTokensPath() + if err != nil { + return err + } + if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil { + return err + } + raw, err := json.MarshalIndent(tokens, "", " ") + if err != nil { + return err + } + return os.WriteFile(path, raw, 0o600) +} + +func DeleteStoredTokens() error { + path, err := storedTokensPath() + if err != nil { + return err + } + if err := os.Remove(path); err != nil && !errors.Is(err, os.ErrNotExist) { + return err + } + return nil +} + +func SaveOAuthTokens(tokens *Tokens) (*StoredTokens, error) { + if tokens == nil { + return nil, fmt.Errorf("tokens are required") + } + now := time.Now().UTC() + stored := &StoredTokens{ + AccessToken: tokens.AccessToken, + RefreshToken: tokens.RefreshToken, + IDToken: tokens.IDToken, + ExpiresAt: inferTokenExpiry(tokens.AccessToken, tokens.IDToken, tokens.ExpiresIn, now), + UpdatedAt: now, + } + if err := SaveStoredTokens(stored); err != nil { + return nil, err + } + return stored, nil +} + +func EnsureFreshStoredTokens(skew time.Duration) (*StoredTokens, error) { + tokens, err := LoadStoredTokens() + if err != nil { + return nil, err + } + if tokens == nil { + return nil, nil + } + + now := time.Now().UTC() + if tokens.ExpiresAt == nil { + tokens.ExpiresAt = inferTokenExpiry(tokens.AccessToken, tokens.IDToken, 0, now) + } + + if tokens.ExpiresAt != nil && now.Add(skew).Before(tokens.ExpiresAt.UTC()) { + return tokens, nil + } + + if tokens.RefreshToken == "" { + return nil, fmt.Errorf("stored OAuth token is expired and has no refresh token") + } + + refreshed, err := RefreshTokens(tokens.RefreshToken) + if err != nil { + return nil, err + } + if refreshed.AccessToken != "" { + tokens.AccessToken = refreshed.AccessToken + } + if refreshed.IDToken != "" { + tokens.IDToken = refreshed.IDToken + } + if refreshed.RefreshToken != "" { + tokens.RefreshToken = refreshed.RefreshToken + } + tokens.ExpiresAt = inferTokenExpiry(tokens.AccessToken, tokens.IDToken, refreshed.ExpiresIn, now) + tokens.UpdatedAt = now + + if err := SaveStoredTokens(tokens); err != nil { + return nil, err + } + return tokens, nil +} + +func inferTokenExpiry(accessToken, idToken string, expiresIn int64, now time.Time) *time.Time { + if expiresIn > 0 { + t := now.Add(time.Duration(expiresIn) * time.Second).UTC() + return &t + } + if t := parseJWTExp(accessToken); t != nil { + return t + } + if t := parseJWTExp(idToken); t != nil { + return t + } + return nil +} + +func parseJWTExp(token string) *time.Time { + parts := strings.Split(token, ".") + if len(parts) != 3 { + return nil + } + payload, err := base64.RawURLEncoding.DecodeString(parts[1]) + if err != nil { + return nil + } + var claims struct { + Exp int64 `json:"exp"` + } + if err := json.Unmarshal(payload, &claims); err != nil || claims.Exp <= 0 { + return nil + } + t := time.Unix(claims.Exp, 0).UTC() + return &t +} diff --git a/clients/ios/Netclode/Features/Chat/ChatInputBar.swift b/clients/ios/Netclode/Features/Chat/ChatInputBar.swift index a26c434f..a4961f41 100644 --- a/clients/ios/Netclode/Features/Chat/ChatInputBar.swift +++ b/clients/ios/Netclode/Features/Chat/ChatInputBar.swift @@ -197,7 +197,8 @@ struct ChatInputBar: View { .foregroundStyle(.white) } } - .disabled(!canSend) + .disabled(!canSend || rightButtonMode != .send) + .keyboardShortcut(.return, modifiers: .command) .opacity(rightButtonMode == .send ? 1 : 0) .scaleEffect(rightButtonMode == .send ? 1 : 0.5) diff --git a/clients/ios/Netclode/Features/Chat/ChatView.swift b/clients/ios/Netclode/Features/Chat/ChatView.swift index 6abe6380..bf216cf9 100644 --- a/clients/ios/Netclode/Features/Chat/ChatView.swift +++ b/clients/ios/Netclode/Features/Chat/ChatView.swift @@ -491,6 +491,9 @@ struct ChatView: View { case .portExposed(let e): PortExposedCard(event: e) + case .portUnexposed(let e): + PortUnexposedCard(event: e) + case .repoClone(let e): RepoCloneCard(event: e) @@ -547,6 +550,9 @@ struct ChatView: View { case .portExposed(let e): result.append(GroupedEvent(id: e.id, event: event, timestamp: e.timestamp)) + case .portUnexposed(let e): + result.append(GroupedEvent(id: e.id, event: event, timestamp: e.timestamp)) + case .repoClone(let e): // Group by repo - update existing entry if same repo, otherwise add new if let index = result.lastIndex(where: { @@ -741,7 +747,6 @@ struct ExposePortSheet: View { let onExpose: (Int) -> Void @Environment(\.dismiss) private var dismiss - @Environment(SettingsStore.self) private var settingsStore @FocusState private var isInputFocused: Bool diff --git a/clients/ios/Netclode/Features/Chat/ToolEventCard.swift b/clients/ios/Netclode/Features/Chat/ToolEventCard.swift index 55268cfd..53d9563d 100644 --- a/clients/ios/Netclode/Features/Chat/ToolEventCard.swift +++ b/clients/ios/Netclode/Features/Chat/ToolEventCard.swift @@ -1576,6 +1576,32 @@ struct PortExposedCard: View { } } +struct PortUnexposedCard: View { + let event: PortUnexposedEvent + + var body: some View { + HStack(spacing: Theme.Spacing.sm) { + HStack(spacing: 4) { + Image(systemName: "network.slash") + .font(.system(size: TypeScale.tiny, weight: .semibold)) + Text(verbatim: "Port \(event.port) removed") + .font(.system(size: TypeScale.caption, weight: .semibold, design: .monospaced)) + } + .foregroundStyle(Theme.Colors.warning) + .padding(.horizontal, 8) + .padding(.vertical, 4) + .background(Theme.Colors.warning.opacity(0.15)) + .clipShape(Capsule()) + + Spacer() + } + .padding(.horizontal, Theme.Spacing.sm) + .padding(.vertical, Theme.Spacing.xs) + .codeCardBackground() + .clipShape(RoundedRectangle(cornerRadius: Theme.Radius.sm)) + } +} + // MARK: - Repo Clone Card struct RepoCloneCard: View { @@ -2039,4 +2065,3 @@ struct SystemEventCard: View { .padding() .background(Theme.Colors.background) } - diff --git a/clients/ios/Netclode/Features/Sessions/ModelPickerSheet.swift b/clients/ios/Netclode/Features/Sessions/ModelPickerSheet.swift index f1a9d037..bff83de8 100644 --- a/clients/ios/Netclode/Features/Sessions/ModelPickerSheet.swift +++ b/clients/ios/Netclode/Features/Sessions/ModelPickerSheet.swift @@ -289,21 +289,30 @@ struct InlineModelPicker: View { // Expanded state - shows all options grouped by provider if isExpanded { - ScrollView { - LazyVStack(spacing: Theme.Spacing.sm) { - ForEach(providerSections) { section in - VStack(alignment: .leading, spacing: 2) { - // Section header - sectionHeader(for: section) - - // Models in this section - ForEach(section.models) { model in - modelRow(for: model) + ScrollViewReader { proxy in + ScrollView { + LazyVStack(spacing: Theme.Spacing.sm) { + ForEach(providerSections) { section in + VStack(alignment: .leading, spacing: 2) { + // Section header + sectionHeader(for: section) + + // Models in this section + ForEach(section.models) { model in + modelRow(for: model) + .id(model.id) + } } } } + .padding(.vertical, Theme.Spacing.xs) + } + .onAppear { + scrollToSelectedModel(using: proxy, animated: false) + } + .onChange(of: selectedModelId) { _, _ in + scrollToSelectedModel(using: proxy, animated: true) } - .padding(.vertical, Theme.Spacing.xs) } .frame(maxHeight: 320) .glassEffect(.regular, in: RoundedRectangle(cornerRadius: Theme.Radius.md)) @@ -379,6 +388,20 @@ struct InlineModelPicker: View { .buttonStyle(.plain) } + private func scrollToSelectedModel(using proxy: ScrollViewProxy, animated: Bool) { + guard models.contains(where: { $0.id == selectedModelId }) else { return } + let action = { + proxy.scrollTo(selectedModelId, anchor: .center) + } + if animated { + withAnimation(.smooth(duration: 0.2)) { + action() + } + } else { + action() + } + } + /// Find the best matching model when exact ID match isn't found private func findBestModel(in models: [PickerModel], preferring preferredId: String) -> PickerModel? { // First try exact match diff --git a/clients/ios/Netclode/Features/Sessions/PromptSheet.swift b/clients/ios/Netclode/Features/Sessions/PromptSheet.swift index 6ed33d4e..d13ab02d 100644 --- a/clients/ios/Netclode/Features/Sessions/PromptSheet.swift +++ b/clients/ios/Netclode/Features/Sessions/PromptSheet.swift @@ -12,10 +12,10 @@ struct PromptSheet: View { @State private var selectedRepos: [String] = [] @State private var repoAccess: RepoAccess = .read @State private var selectedSdkType: SdkType = .claude - @State private var selectedClaudeModelId: String = UnifiedModelsStore.defaultClaudeModelId - @State private var selectedOpenCodeModelId: String = UnifiedModelsStore.defaultOpenCodeModelId - @State private var selectedCopilotModelId: String = UnifiedModelsStore.defaultCopilotModelId - @State private var selectedCodexModelId: String = UnifiedModelsStore.defaultCodexModelId + @State private var selectedClaudeModelId = "" + @State private var selectedOpenCodeModelId = "" + @State private var selectedCopilotModelId = "" + @State private var selectedCodexModelId = "" @State private var isSubmitting = false @State private var canSubmit = false @State private var showModelDropdown = false @@ -473,6 +473,7 @@ struct PromptSheet: View { } .onAppear { isFocused = true + initializePreferredModels() // Initialize resource defaults from server (if already loaded) if let limits = modelsStore.resourceLimits { vcpus = limits.defaultVcpus @@ -486,6 +487,8 @@ struct PromptSheet: View { withAnimation(.smooth(duration: 0.2)) { showModelDropdown = false } + + reconcileSelectedModel(for: selectedSdkType) } .onChange(of: showModelDropdown) { _, isExpanded in // Dismiss keyboard when opening model dropdown @@ -512,6 +515,18 @@ struct PromptSheet: View { memoryMB = limits.defaultMemoryMB } } + .onChange(of: modelsStore.models(for: .claude).map(\.id)) { _, _ in + reconcileSelectedModel(for: .claude) + } + .onChange(of: modelsStore.models(for: .opencode).map(\.id)) { _, _ in + reconcileSelectedModel(for: .opencode) + } + .onChange(of: modelsStore.models(for: .copilot).map(\.id)) { _, _ in + reconcileSelectedModel(for: .copilot) + } + .onChange(of: modelsStore.models(for: .codex).map(\.id)) { _, _ in + reconcileSelectedModel(for: .codex) + } } .presentationDetents([.medium, .large]) .presentationDragIndicator(.visible) @@ -545,11 +560,88 @@ struct PromptSheet: View { return "\(mb) MB" } + private func initializePreferredModels() { + setSelectedModelId( + modelsStore.preferredModelId( + for: .claude, + lastUsedModelId: settingsStore.lastUsedModelId(for: .claude) + ), + for: .claude + ) + setSelectedModelId( + modelsStore.preferredModelId( + for: .opencode, + lastUsedModelId: settingsStore.lastUsedModelId(for: .opencode) + ), + for: .opencode + ) + setSelectedModelId( + modelsStore.preferredModelId( + for: .copilot, + lastUsedModelId: settingsStore.lastUsedModelId(for: .copilot) + ), + for: .copilot + ) + setSelectedModelId( + modelsStore.preferredModelId( + for: .codex, + lastUsedModelId: settingsStore.lastUsedModelId(for: .codex) + ), + for: .codex + ) + } + + private func currentSelectedModelId(for sdkType: SdkType) -> String { + switch sdkType { + case .claude: + return selectedClaudeModelId + case .opencode: + return selectedOpenCodeModelId + case .copilot: + return selectedCopilotModelId + case .codex: + return selectedCodexModelId + } + } + + private func setSelectedModelId(_ modelId: String, for sdkType: SdkType) { + switch sdkType { + case .claude: + selectedClaudeModelId = modelId + case .opencode: + selectedOpenCodeModelId = modelId + case .copilot: + selectedCopilotModelId = modelId + case .codex: + selectedCodexModelId = modelId + } + } + + private func reconcileSelectedModel(for sdkType: SdkType) { + let current = currentSelectedModelId(for: sdkType) + let models = modelsStore.models(for: sdkType) + let hasPersistedLastUsed = settingsStore.lastUsedModelId(for: sdkType) != nil + let isCurrentValid = !current.isEmpty && models.contains(where: { $0.id == current }) + if hasPersistedLastUsed && isCurrentValid { + return + } + + let preferred = modelsStore.preferredModelId( + for: sdkType, + lastUsedModelId: settingsStore.lastUsedModelId(for: sdkType) + ) + if preferred == current { + return + } + setSelectedModelId(preferred, for: sdkType) + } + private func submitPrompt() { let text = promptText.trimmingCharacters(in: .whitespacesAndNewlines) guard !text.isEmpty else { return } isSubmitting = true + sessionStore.clearPendingCreationError() if settingsStore.hapticFeedbackEnabled { HapticFeedback.medium() @@ -579,6 +671,10 @@ struct PromptSheet: View { modelParam = selectedCodexModelId } + if let modelParam { + settingsStore.setLastUsedModelId(modelParam, for: selectedSdkType) + } + // Build network config (only if tailnet access is requested) var networkConfig: NetworkConfig? = nil if tailnetAccess { diff --git a/clients/ios/Netclode/Features/Sessions/SessionsView.swift b/clients/ios/Netclode/Features/Sessions/SessionsView.swift index 87d78ce6..1fc77b5a 100644 --- a/clients/ios/Netclode/Features/Sessions/SessionsView.swift +++ b/clients/ios/Netclode/Features/Sessions/SessionsView.swift @@ -85,6 +85,11 @@ struct SessionsView: View { selectedSession = session } } + .onChange(of: sessionStore.pendingCreationError) { _, newError in + if newError != nil, settingsStore.hapticFeedbackEnabled { + HapticFeedback.error() + } + } .onAppear { if connectService.connectionState.isConnected { connectService.send(.sessionList) @@ -125,6 +130,18 @@ struct SessionsView: View { Text("This will permanently delete \"\(session.name)\" and all its data.") } } + .alert("Session Creation Failed", isPresented: .init( + get: { sessionStore.pendingCreationError != nil }, + set: { if !$0 { sessionStore.clearPendingCreationError() } } + )) { + Button("OK") { + sessionStore.clearPendingCreationError() + } + } message: { + if let error = sessionStore.pendingCreationError { + Text(error) + } + } } private var connectionColor: Color { diff --git a/clients/ios/Netclode/Features/Workspace/PreviewsView.swift b/clients/ios/Netclode/Features/Workspace/PreviewsView.swift index 3e1c4e67..a1b9476b 100644 --- a/clients/ios/Netclode/Features/Workspace/PreviewsView.swift +++ b/clients/ios/Netclode/Features/Workspace/PreviewsView.swift @@ -10,19 +10,25 @@ struct PreviewsView: View { @State private var portToExpose = "" @State private var isContentVisible = false - /// All port_exposed events for this session - var portEvents: [PortExposedEvent] { - eventStore.events(for: sessionId).compactMap { event in - if case .portExposed(let e) = event { - return e + /// Currently active exposed ports derived from port_exposed/port_unexposed event history. + var activePortEvents: [PortExposedEvent] { + var byPort: [Int: PortExposedEvent] = [:] + for event in eventStore.events(for: sessionId) { + switch event { + case .portExposed(let e): + byPort[e.port] = e + case .portUnexposed(let e): + byPort.removeValue(forKey: e.port) + default: + break } - return nil } + return byPort.values.sorted { $0.port < $1.port } } var body: some View { ZStack(alignment: .bottomTrailing) { - if portEvents.isEmpty { + if activePortEvents.isEmpty { ContentUnavailableView { Label("No Previews", systemImage: "globe") } description: { @@ -31,8 +37,10 @@ struct PreviewsView: View { } else { ScrollView { LazyVStack(spacing: Theme.Spacing.md) { - ForEach(portEvents) { event in - PreviewCard(event: event) + ForEach(activePortEvents) { event in + PreviewCard(event: event) { port in + connectService.send(.portUnexpose(sessionId: sessionId, port: port)) + } } } .padding() @@ -70,6 +78,7 @@ struct PreviewsView: View { struct PreviewCard: View { let event: PortExposedEvent + let onRemove: (Int) -> Void var body: some View { GlassCard { @@ -104,6 +113,11 @@ struct PreviewCard: View { } label: { Label("Copy URL", systemImage: "doc.on.doc") } + Button(role: .destructive) { + onRemove(event.port) + } label: { + Label("Remove Port", systemImage: "trash") + } } label: { HStack(spacing: 4) { Text("Open") diff --git a/clients/ios/Netclode/Generated/netclode/v1/agent.pb.swift b/clients/ios/Netclode/Generated/netclode/v1/agent.pb.swift index 731fb341..7db52784 100644 --- a/clients/ios/Netclode/Generated/netclode/v1/agent.pb.swift +++ b/clients/ios/Netclode/Generated/netclode/v1/agent.pb.swift @@ -192,6 +192,15 @@ public struct Netclode_V1_ControlPlaneMessage: Sendable { set {message = .sessionAssigned(newValue)} } + /// Update Codex OAuth tokens for an active session. + public var updateCodexAuth: Netclode_V1_UpdateCodexAuth { + get { + if case .updateCodexAuth(let v)? = message {return v} + return Netclode_V1_UpdateCodexAuth() + } + set {message = .updateCodexAuth(newValue)} + } + public var unknownFields = SwiftProtobuf.UnknownStorage() public enum OneOf_Message: Equatable, Sendable { @@ -213,6 +222,8 @@ public struct Netclode_V1_ControlPlaneMessage: Sendable { case updateGitCredentials(Netclode_V1_UpdateGitCredentials) /// Session assigned (warm pool mode) - pushed when claim binds case sessionAssigned(Netclode_V1_SessionAssigned) + /// Update Codex OAuth tokens for an active session. + case updateCodexAuth(Netclode_V1_UpdateCodexAuth) } @@ -231,11 +242,11 @@ public struct Netclode_V1_SessionAssigned: Sendable { /// Full session configuration public var config: Netclode_V1_SessionConfig { - get {return _config ?? Netclode_V1_SessionConfig()} + get {_config ?? Netclode_V1_SessionConfig()} set {_config = newValue} } /// Returns true if `config` has been explicitly set. - public var hasConfig: Bool {return self._config != nil} + public var hasConfig: Bool {self._config != nil} /// Clears the value of `config`. Subsequent reads from it will return its default value. public mutating func clearConfig() {self._config = nil} @@ -254,11 +265,11 @@ public struct Netclode_V1_AgentRegister: Sendable { /// Session this agent is servicing (empty for warm pool mode) public var sessionID: String { - get {return _sessionID ?? String()} + get {_sessionID ?? String()} set {_sessionID = newValue} } /// Returns true if `sessionID` has been explicitly set. - public var hasSessionID: Bool {return self._sessionID != nil} + public var hasSessionID: Bool {self._sessionID != nil} /// Clears the value of `sessionID`. Subsequent reads from it will return its default value. public mutating func clearSessionID() {self._sessionID = nil} @@ -267,21 +278,21 @@ public struct Netclode_V1_AgentRegister: Sendable { /// Pod name for warm pool mode (deprecated, use k8s_token) public var podName: String { - get {return _podName ?? String()} + get {_podName ?? String()} set {_podName = newValue} } /// Returns true if `podName` has been explicitly set. - public var hasPodName: Bool {return self._podName != nil} + public var hasPodName: Bool {self._podName != nil} /// Clears the value of `podName`. Subsequent reads from it will return its default value. public mutating func clearPodName() {self._podName = nil} /// Kubernetes ServiceAccount token for identity verification public var k8SToken: String { - get {return _k8SToken ?? String()} + get {_k8SToken ?? String()} set {_k8SToken = newValue} } /// Returns true if `k8SToken` has been explicitly set. - public var hasK8SToken: Bool {return self._k8SToken != nil} + public var hasK8SToken: Bool {self._k8SToken != nil} /// Clears the value of `k8SToken`. Subsequent reads from it will return its default value. public mutating func clearK8SToken() {self._k8SToken = nil} @@ -513,21 +524,21 @@ public struct Netclode_V1_AgentRegistered: Sendable { /// Error message if registration failed public var error: String { - get {return _error ?? String()} + get {_error ?? String()} set {_error = newValue} } /// Returns true if `error` has been explicitly set. - public var hasError: Bool {return self._error != nil} + public var hasError: Bool {self._error != nil} /// Clears the value of `error`. Subsequent reads from it will return its default value. public mutating func clearError() {self._error = nil} /// Session configuration for the agent public var config: Netclode_V1_SessionConfig { - get {return _config ?? Netclode_V1_SessionConfig()} + get {_config ?? Netclode_V1_SessionConfig()} set {_config = newValue} } /// Returns true if `config` has been explicitly set. - public var hasConfig: Bool {return self._config != nil} + public var hasConfig: Bool {self._config != nil} /// Clears the value of `config`. Subsequent reads from it will return its default value. public mutating func clearConfig() {self._config = nil} @@ -606,11 +617,11 @@ public struct Netclode_V1_GetGitDiffRequest: Sendable { /// Specific file, or all files if empty public var file: String { - get {return _file ?? String()} + get {_file ?? String()} set {_file = newValue} } /// Returns true if `file` has been explicitly set. - public var hasFile: Bool {return self._file != nil} + public var hasFile: Bool {self._file != nil} /// Clears the value of `file`. Subsequent reads from it will return its default value. public mutating func clearFile() {self._file = nil} @@ -695,6 +706,32 @@ public struct Netclode_V1_UpdateGitCredentials: Sendable { public init() {} } +/// UpdateCodexAuth updates short-lived Codex OAuth tokens for the running agent. +public struct Netclode_V1_UpdateCodexAuth: Sendable { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + public var accessToken: String = String() + + public var idToken: String = String() + + public var expiresAt: SwiftProtobuf.Google_Protobuf_Timestamp { + get {_expiresAt ?? SwiftProtobuf.Google_Protobuf_Timestamp()} + set {_expiresAt = newValue} + } + /// Returns true if `expiresAt` has been explicitly set. + public var hasExpiresAt: Bool {self._expiresAt != nil} + /// Clears the value of `expiresAt`. Subsequent reads from it will return its default value. + public mutating func clearExpiresAt() {self._expiresAt = nil} + + public var unknownFields = SwiftProtobuf.UnknownStorage() + + public init() {} + + fileprivate var _expiresAt: SwiftProtobuf.Google_Protobuf_Timestamp? = nil +} + // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "netclode.v1" @@ -836,7 +873,7 @@ extension Netclode_V1_AgentMessage: SwiftProtobuf.Message, SwiftProtobuf._Messag extension Netclode_V1_ControlPlaneMessage: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = _protobuf_package + ".ControlPlaneMessage" - public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}registered\0\u{3}execute_prompt\0\u{1}interrupt\0\u{3}generate_title\0\u{3}get_git_status\0\u{3}get_git_diff\0\u{3}terminal_input\0\u{3}update_git_credentials\0\u{3}session_assigned\0") + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}registered\0\u{3}execute_prompt\0\u{1}interrupt\0\u{3}generate_title\0\u{3}get_git_status\0\u{3}get_git_diff\0\u{3}terminal_input\0\u{3}update_git_credentials\0\u{3}session_assigned\0\u{3}update_codex_auth\0") public mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { @@ -961,6 +998,19 @@ extension Netclode_V1_ControlPlaneMessage: SwiftProtobuf.Message, SwiftProtobuf. self.message = .sessionAssigned(v) } }() + case 10: try { + var v: Netclode_V1_UpdateCodexAuth? + var hadOneofValue = false + if let current = self.message { + hadOneofValue = true + if case .updateCodexAuth(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.message = .updateCodexAuth(v) + } + }() default: break } } @@ -1008,6 +1058,10 @@ extension Netclode_V1_ControlPlaneMessage: SwiftProtobuf.Message, SwiftProtobuf. guard case .sessionAssigned(let v)? = self.message else { preconditionFailure() } try visitor.visitSingularMessageField(value: v, fieldNumber: 9) }() + case .updateCodexAuth?: try { + guard case .updateCodexAuth(let v)? = self.message else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 10) + }() case nil: break } try unknownFields.traverse(visitor: &visitor) @@ -1834,3 +1888,47 @@ extension Netclode_V1_UpdateGitCredentials: SwiftProtobuf.Message, SwiftProtobuf return true } } + +extension Netclode_V1_UpdateCodexAuth: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + public static let protoMessageName: String = _protobuf_package + ".UpdateCodexAuth" + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}access_token\0\u{3}id_token\0\u{3}expires_at\0") + + public mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularStringField(value: &self.accessToken) }() + case 2: try { try decoder.decodeSingularStringField(value: &self.idToken) }() + case 3: try { try decoder.decodeSingularMessageField(value: &self._expiresAt) }() + default: break + } + } + } + + public func traverse(visitor: inout V) throws { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 + if !self.accessToken.isEmpty { + try visitor.visitSingularStringField(value: self.accessToken, fieldNumber: 1) + } + if !self.idToken.isEmpty { + try visitor.visitSingularStringField(value: self.idToken, fieldNumber: 2) + } + try { if let v = self._expiresAt { + try visitor.visitSingularMessageField(value: v, fieldNumber: 3) + } }() + try unknownFields.traverse(visitor: &visitor) + } + + public static func ==(lhs: Netclode_V1_UpdateCodexAuth, rhs: Netclode_V1_UpdateCodexAuth) -> Bool { + if lhs.accessToken != rhs.accessToken {return false} + if lhs.idToken != rhs.idToken {return false} + if lhs._expiresAt != rhs._expiresAt {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} diff --git a/clients/ios/Netclode/Generated/netclode/v1/client.pb.swift b/clients/ios/Netclode/Generated/netclode/v1/client.pb.swift index 39737e6a..cb49b40f 100644 --- a/clients/ios/Netclode/Generated/netclode/v1/client.pb.swift +++ b/clients/ios/Netclode/Generated/netclode/v1/client.pb.swift @@ -20,6 +20,52 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP typealias Version = _2 } +public enum Netclode_V1_CodexAuthState: SwiftProtobuf.Enum, Swift.CaseIterable { + public typealias RawValue = Int + case unspecified // = 0 + case unauthenticated // = 1 + case pending // = 2 + case ready // = 3 + case error // = 4 + case UNRECOGNIZED(Int) + + public init() { + self = .unspecified + } + + public init?(rawValue: Int) { + switch rawValue { + case 0: self = .unspecified + case 1: self = .unauthenticated + case 2: self = .pending + case 3: self = .ready + case 4: self = .error + default: self = .UNRECOGNIZED(rawValue) + } + } + + public var rawValue: Int { + switch self { + case .unspecified: return 0 + case .unauthenticated: return 1 + case .pending: return 2 + case .ready: return 3 + case .error: return 4 + case .UNRECOGNIZED(let i): return i + } + } + + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [Netclode_V1_CodexAuthState] = [ + .unspecified, + .unauthenticated, + .pending, + .ready, + .error, + ] + +} + /// ClientMessage is the union of all client-to-server messages. public struct Netclode_V1_ClientMessage: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the @@ -207,6 +253,39 @@ public struct Netclode_V1_ClientMessage: Sendable { set {message = .getResourceLimits(newValue)} } + /// Backend-managed Codex OAuth flow + public var codexAuthStart: Netclode_V1_CodexAuthStartRequest { + get { + if case .codexAuthStart(let v)? = message {return v} + return Netclode_V1_CodexAuthStartRequest() + } + set {message = .codexAuthStart(newValue)} + } + + public var codexAuthStatus: Netclode_V1_CodexAuthStatusRequest { + get { + if case .codexAuthStatus(let v)? = message {return v} + return Netclode_V1_CodexAuthStatusRequest() + } + set {message = .codexAuthStatus(newValue)} + } + + public var codexAuthLogout: Netclode_V1_CodexAuthLogoutRequest { + get { + if case .codexAuthLogout(let v)? = message {return v} + return Netclode_V1_CodexAuthLogoutRequest() + } + set {message = .codexAuthLogout(newValue)} + } + + public var unexposePort: Netclode_V1_UnexposePortRequest { + get { + if case .unexposePort(let v)? = message {return v} + return Netclode_V1_UnexposePortRequest() + } + set {message = .unexposePort(newValue)} + } + public var unknownFields = SwiftProtobuf.UnknownStorage() public enum OneOf_Message: Equatable, Sendable { @@ -235,6 +314,11 @@ public struct Netclode_V1_ClientMessage: Sendable { case updateRepoAccess(Netclode_V1_UpdateRepoAccessRequest) /// Resource limits case getResourceLimits(Netclode_V1_GetResourceLimitsRequest) + /// Backend-managed Codex OAuth flow + case codexAuthStart(Netclode_V1_CodexAuthStartRequest) + case codexAuthStatus(Netclode_V1_CodexAuthStatusRequest) + case codexAuthLogout(Netclode_V1_CodexAuthLogoutRequest) + case unexposePort(Netclode_V1_UnexposePortRequest) } @@ -414,6 +498,39 @@ public struct Netclode_V1_ServerMessage: Sendable { set {message = .resourceLimits(newValue)} } + /// Backend-managed Codex OAuth flow + public var codexAuthStarted: Netclode_V1_CodexAuthStartedResponse { + get { + if case .codexAuthStarted(let v)? = message {return v} + return Netclode_V1_CodexAuthStartedResponse() + } + set {message = .codexAuthStarted(newValue)} + } + + public var codexAuthStatus: Netclode_V1_CodexAuthStatusResponse { + get { + if case .codexAuthStatus(let v)? = message {return v} + return Netclode_V1_CodexAuthStatusResponse() + } + set {message = .codexAuthStatus(newValue)} + } + + public var codexAuthLoggedOut: Netclode_V1_CodexAuthLoggedOutResponse { + get { + if case .codexAuthLoggedOut(let v)? = message {return v} + return Netclode_V1_CodexAuthLoggedOutResponse() + } + set {message = .codexAuthLoggedOut(newValue)} + } + + public var portUnexposed: Netclode_V1_PortUnexposedResponse { + get { + if case .portUnexposed(let v)? = message {return v} + return Netclode_V1_PortUnexposedResponse() + } + set {message = .portUnexposed(newValue)} + } + public var unknownFields = SwiftProtobuf.UnknownStorage() public enum OneOf_Message: Equatable, Sendable { @@ -442,6 +559,11 @@ public struct Netclode_V1_ServerMessage: Sendable { case repoAccessUpdated(Netclode_V1_RepoAccessUpdatedResponse) /// Resource limits case resourceLimits(Netclode_V1_ResourceLimitsResponse) + /// Backend-managed Codex OAuth flow + case codexAuthStarted(Netclode_V1_CodexAuthStartedResponse) + case codexAuthStatus(Netclode_V1_CodexAuthStatusResponse) + case codexAuthLoggedOut(Netclode_V1_CodexAuthLoggedOutResponse) + case portUnexposed(Netclode_V1_PortUnexposedResponse) } @@ -464,6 +586,34 @@ public struct Netclode_V1_NetworkConfig: Sendable { public init() {} } +/// CodexOAuthTokens contains ChatGPT OAuth tokens for Codex sessions. +public struct Netclode_V1_CodexOAuthTokens: Sendable { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + public var accessToken: String = String() + + public var idToken: String = String() + + public var refreshToken: String = String() + + public var expiresAt: SwiftProtobuf.Google_Protobuf_Timestamp { + get {_expiresAt ?? SwiftProtobuf.Google_Protobuf_Timestamp()} + set {_expiresAt = newValue} + } + /// Returns true if `expiresAt` has been explicitly set. + public var hasExpiresAt: Bool {self._expiresAt != nil} + /// Clears the value of `expiresAt`. Subsequent reads from it will return its default value. + public mutating func clearExpiresAt() {self._expiresAt = nil} + + public var unknownFields = SwiftProtobuf.UnknownStorage() + + public init() {} + + fileprivate var _expiresAt: SwiftProtobuf.Google_Protobuf_Timestamp? = nil +} + public struct Netclode_V1_CreateSessionRequest: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for @@ -471,21 +621,21 @@ public struct Netclode_V1_CreateSessionRequest: Sendable { /// Client-generated ID for request correlation public var requestID: String { - get {return _requestID ?? String()} + get {_requestID ?? String()} set {_requestID = newValue} } /// Returns true if `requestID` has been explicitly set. - public var hasRequestID: Bool {return self._requestID != nil} + public var hasRequestID: Bool {self._requestID != nil} /// Clears the value of `requestID`. Subsequent reads from it will return its default value. public mutating func clearRequestID() {self._requestID = nil} /// Initial session name public var name: String { - get {return _name ?? String()} + get {_name ?? String()} set {_name = newValue} } /// Returns true if `name` has been explicitly set. - public var hasName: Bool {return self._name != nil} + public var hasName: Bool {self._name != nil} /// Clears the value of `name`. Subsequent reads from it will return its default value. public mutating func clearName() {self._name = nil} @@ -494,74 +644,84 @@ public struct Netclode_V1_CreateSessionRequest: Sendable { /// Permission level for repository public var repoAccess: Netclode_V1_RepoAccess { - get {return _repoAccess ?? .unspecified} + get {_repoAccess ?? .unspecified} set {_repoAccess = newValue} } /// Returns true if `repoAccess` has been explicitly set. - public var hasRepoAccess: Bool {return self._repoAccess != nil} + public var hasRepoAccess: Bool {self._repoAccess != nil} /// Clears the value of `repoAccess`. Subsequent reads from it will return its default value. public mutating func clearRepoAccess() {self._repoAccess = nil} /// Optional prompt to send immediately after creation public var initialPrompt: String { - get {return _initialPrompt ?? String()} + get {_initialPrompt ?? String()} set {_initialPrompt = newValue} } /// Returns true if `initialPrompt` has been explicitly set. - public var hasInitialPrompt: Bool {return self._initialPrompt != nil} + public var hasInitialPrompt: Bool {self._initialPrompt != nil} /// Clears the value of `initialPrompt`. Subsequent reads from it will return its default value. public mutating func clearInitialPrompt() {self._initialPrompt = nil} /// SDK to use (defaults to CLAUDE) public var sdkType: Netclode_V1_SdkType { - get {return _sdkType ?? .unspecified} + get {_sdkType ?? .unspecified} set {_sdkType = newValue} } /// Returns true if `sdkType` has been explicitly set. - public var hasSdkType: Bool {return self._sdkType != nil} + public var hasSdkType: Bool {self._sdkType != nil} /// Clears the value of `sdkType`. Subsequent reads from it will return its default value. public mutating func clearSdkType() {self._sdkType = nil} /// Model ID (e.g., "claude-sonnet-4-0", "gpt-4o") public var model: String { - get {return _model ?? String()} + get {_model ?? String()} set {_model = newValue} } /// Returns true if `model` has been explicitly set. - public var hasModel: Bool {return self._model != nil} + public var hasModel: Bool {self._model != nil} /// Clears the value of `model`. Subsequent reads from it will return its default value. public mutating func clearModel() {self._model = nil} /// Backend for Copilot SDK (GitHub or Anthropic) public var copilotBackend: Netclode_V1_CopilotBackend { - get {return _copilotBackend ?? .unspecified} + get {_copilotBackend ?? .unspecified} set {_copilotBackend = newValue} } /// Returns true if `copilotBackend` has been explicitly set. - public var hasCopilotBackend: Bool {return self._copilotBackend != nil} + public var hasCopilotBackend: Bool {self._copilotBackend != nil} /// Clears the value of `copilotBackend`. Subsequent reads from it will return its default value. public mutating func clearCopilotBackend() {self._copilotBackend = nil} /// Network configuration (defaults to enabled) public var networkConfig: Netclode_V1_NetworkConfig { - get {return _networkConfig ?? Netclode_V1_NetworkConfig()} + get {_networkConfig ?? Netclode_V1_NetworkConfig()} set {_networkConfig = newValue} } /// Returns true if `networkConfig` has been explicitly set. - public var hasNetworkConfig: Bool {return self._networkConfig != nil} + public var hasNetworkConfig: Bool {self._networkConfig != nil} /// Clears the value of `networkConfig`. Subsequent reads from it will return its default value. public mutating func clearNetworkConfig() {self._networkConfig = nil} /// Custom VM resources (bypasses warm pool if set) public var resources: Netclode_V1_SandboxResources { - get {return _resources ?? Netclode_V1_SandboxResources()} + get {_resources ?? Netclode_V1_SandboxResources()} set {_resources = newValue} } /// Returns true if `resources` has been explicitly set. - public var hasResources: Bool {return self._resources != nil} + public var hasResources: Bool {self._resources != nil} /// Clears the value of `resources`. Subsequent reads from it will return its default value. public mutating func clearResources() {self._resources = nil} + /// Session-scoped OAuth tokens for Codex :oauth models + public var codexOauthTokens: Netclode_V1_CodexOAuthTokens { + get {_codexOauthTokens ?? Netclode_V1_CodexOAuthTokens()} + set {_codexOauthTokens = newValue} + } + /// Returns true if `codexOauthTokens` has been explicitly set. + public var hasCodexOauthTokens: Bool {self._codexOauthTokens != nil} + /// Clears the value of `codexOauthTokens`. Subsequent reads from it will return its default value. + public mutating func clearCodexOauthTokens() {self._codexOauthTokens = nil} + public var unknownFields = SwiftProtobuf.UnknownStorage() public init() {} @@ -575,6 +735,7 @@ public struct Netclode_V1_CreateSessionRequest: Sendable { fileprivate var _copilotBackend: Netclode_V1_CopilotBackend? = nil fileprivate var _networkConfig: Netclode_V1_NetworkConfig? = nil fileprivate var _resources: Netclode_V1_SandboxResources? = nil + fileprivate var _codexOauthTokens: Netclode_V1_CodexOAuthTokens? = nil } public struct Netclode_V1_ListSessionsRequest: Sendable { @@ -583,11 +744,11 @@ public struct Netclode_V1_ListSessionsRequest: Sendable { // methods supported on all messages. public var requestID: String { - get {return _requestID ?? String()} + get {_requestID ?? String()} set {_requestID = newValue} } /// Returns true if `requestID` has been explicitly set. - public var hasRequestID: Bool {return self._requestID != nil} + public var hasRequestID: Bool {self._requestID != nil} /// Clears the value of `requestID`. Subsequent reads from it will return its default value. public mutating func clearRequestID() {self._requestID = nil} @@ -604,11 +765,11 @@ public struct Netclode_V1_OpenSessionRequest: Sendable { // methods supported on all messages. public var requestID: String { - get {return _requestID ?? String()} + get {_requestID ?? String()} set {_requestID = newValue} } /// Returns true if `requestID` has been explicitly set. - public var hasRequestID: Bool {return self._requestID != nil} + public var hasRequestID: Bool {self._requestID != nil} /// Clears the value of `requestID`. Subsequent reads from it will return its default value. public mutating func clearRequestID() {self._requestID = nil} @@ -616,21 +777,21 @@ public struct Netclode_V1_OpenSessionRequest: Sendable { /// Cursor: return entries after this stream ID public var afterStreamID: String { - get {return _afterStreamID ?? String()} + get {_afterStreamID ?? String()} set {_afterStreamID = newValue} } /// Returns true if `afterStreamID` has been explicitly set. - public var hasAfterStreamID: Bool {return self._afterStreamID != nil} + public var hasAfterStreamID: Bool {self._afterStreamID != nil} /// Clears the value of `afterStreamID`. Subsequent reads from it will return its default value. public mutating func clearAfterStreamID() {self._afterStreamID = nil} /// Max entries to return (default: all) public var limit: Int32 { - get {return _limit ?? 0} + get {_limit ?? 0} set {_limit = newValue} } /// Returns true if `limit` has been explicitly set. - public var hasLimit: Bool {return self._limit != nil} + public var hasLimit: Bool {self._limit != nil} /// Clears the value of `limit`. Subsequent reads from it will return its default value. public mutating func clearLimit() {self._limit = nil} @@ -649,11 +810,11 @@ public struct Netclode_V1_ResumeSessionRequest: Sendable { // methods supported on all messages. public var requestID: String { - get {return _requestID ?? String()} + get {_requestID ?? String()} set {_requestID = newValue} } /// Returns true if `requestID` has been explicitly set. - public var hasRequestID: Bool {return self._requestID != nil} + public var hasRequestID: Bool {self._requestID != nil} /// Clears the value of `requestID`. Subsequent reads from it will return its default value. public mutating func clearRequestID() {self._requestID = nil} @@ -672,11 +833,11 @@ public struct Netclode_V1_PauseSessionRequest: Sendable { // methods supported on all messages. public var requestID: String { - get {return _requestID ?? String()} + get {_requestID ?? String()} set {_requestID = newValue} } /// Returns true if `requestID` has been explicitly set. - public var hasRequestID: Bool {return self._requestID != nil} + public var hasRequestID: Bool {self._requestID != nil} /// Clears the value of `requestID`. Subsequent reads from it will return its default value. public mutating func clearRequestID() {self._requestID = nil} @@ -695,11 +856,11 @@ public struct Netclode_V1_DeleteSessionRequest: Sendable { // methods supported on all messages. public var requestID: String { - get {return _requestID ?? String()} + get {_requestID ?? String()} set {_requestID = newValue} } /// Returns true if `requestID` has been explicitly set. - public var hasRequestID: Bool {return self._requestID != nil} + public var hasRequestID: Bool {self._requestID != nil} /// Clears the value of `requestID`. Subsequent reads from it will return its default value. public mutating func clearRequestID() {self._requestID = nil} @@ -718,11 +879,11 @@ public struct Netclode_V1_DeleteAllSessionsRequest: Sendable { // methods supported on all messages. public var requestID: String { - get {return _requestID ?? String()} + get {_requestID ?? String()} set {_requestID = newValue} } /// Returns true if `requestID` has been explicitly set. - public var hasRequestID: Bool {return self._requestID != nil} + public var hasRequestID: Bool {self._requestID != nil} /// Clears the value of `requestID`. Subsequent reads from it will return its default value. public mutating func clearRequestID() {self._requestID = nil} @@ -739,11 +900,11 @@ public struct Netclode_V1_SendPromptRequest: Sendable { // methods supported on all messages. public var requestID: String { - get {return _requestID ?? String()} + get {_requestID ?? String()} set {_requestID = newValue} } /// Returns true if `requestID` has been explicitly set. - public var hasRequestID: Bool {return self._requestID != nil} + public var hasRequestID: Bool {self._requestID != nil} /// Clears the value of `requestID`. Subsequent reads from it will return its default value. public mutating func clearRequestID() {self._requestID = nil} @@ -764,11 +925,11 @@ public struct Netclode_V1_InterruptPromptRequest: Sendable { // methods supported on all messages. public var requestID: String { - get {return _requestID ?? String()} + get {_requestID ?? String()} set {_requestID = newValue} } /// Returns true if `requestID` has been explicitly set. - public var hasRequestID: Bool {return self._requestID != nil} + public var hasRequestID: Bool {self._requestID != nil} /// Clears the value of `requestID`. Subsequent reads from it will return its default value. public mutating func clearRequestID() {self._requestID = nil} @@ -787,11 +948,11 @@ public struct Netclode_V1_TerminalInputRequest: Sendable { // methods supported on all messages. public var requestID: String { - get {return _requestID ?? String()} + get {_requestID ?? String()} set {_requestID = newValue} } /// Returns true if `requestID` has been explicitly set. - public var hasRequestID: Bool {return self._requestID != nil} + public var hasRequestID: Bool {self._requestID != nil} /// Clears the value of `requestID`. Subsequent reads from it will return its default value. public mutating func clearRequestID() {self._requestID = nil} @@ -812,11 +973,11 @@ public struct Netclode_V1_TerminalResizeRequest: Sendable { // methods supported on all messages. public var requestID: String { - get {return _requestID ?? String()} + get {_requestID ?? String()} set {_requestID = newValue} } /// Returns true if `requestID` has been explicitly set. - public var hasRequestID: Bool {return self._requestID != nil} + public var hasRequestID: Bool {self._requestID != nil} /// Clears the value of `requestID`. Subsequent reads from it will return its default value. public mutating func clearRequestID() {self._requestID = nil} @@ -839,11 +1000,36 @@ public struct Netclode_V1_ExposePortRequest: Sendable { // methods supported on all messages. public var requestID: String { - get {return _requestID ?? String()} + get {_requestID ?? String()} + set {_requestID = newValue} + } + /// Returns true if `requestID` has been explicitly set. + public var hasRequestID: Bool {self._requestID != nil} + /// Clears the value of `requestID`. Subsequent reads from it will return its default value. + public mutating func clearRequestID() {self._requestID = nil} + + public var sessionID: String = String() + + public var port: Int32 = 0 + + public var unknownFields = SwiftProtobuf.UnknownStorage() + + public init() {} + + fileprivate var _requestID: String? = nil +} + +public struct Netclode_V1_UnexposePortRequest: Sendable { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + public var requestID: String { + get {_requestID ?? String()} set {_requestID = newValue} } /// Returns true if `requestID` has been explicitly set. - public var hasRequestID: Bool {return self._requestID != nil} + public var hasRequestID: Bool {self._requestID != nil} /// Clears the value of `requestID`. Subsequent reads from it will return its default value. public mutating func clearRequestID() {self._requestID = nil} @@ -864,11 +1050,11 @@ public struct Netclode_V1_SyncRequest: Sendable { // methods supported on all messages. public var requestID: String { - get {return _requestID ?? String()} + get {_requestID ?? String()} set {_requestID = newValue} } /// Returns true if `requestID` has been explicitly set. - public var hasRequestID: Bool {return self._requestID != nil} + public var hasRequestID: Bool {self._requestID != nil} /// Clears the value of `requestID`. Subsequent reads from it will return its default value. public mutating func clearRequestID() {self._requestID = nil} @@ -885,11 +1071,11 @@ public struct Netclode_V1_ListGitHubReposRequest: Sendable { // methods supported on all messages. public var requestID: String { - get {return _requestID ?? String()} + get {_requestID ?? String()} set {_requestID = newValue} } /// Returns true if `requestID` has been explicitly set. - public var hasRequestID: Bool {return self._requestID != nil} + public var hasRequestID: Bool {self._requestID != nil} /// Clears the value of `requestID`. Subsequent reads from it will return its default value. public mutating func clearRequestID() {self._requestID = nil} @@ -906,11 +1092,11 @@ public struct Netclode_V1_GitStatusRequest: Sendable { // methods supported on all messages. public var requestID: String { - get {return _requestID ?? String()} + get {_requestID ?? String()} set {_requestID = newValue} } /// Returns true if `requestID` has been explicitly set. - public var hasRequestID: Bool {return self._requestID != nil} + public var hasRequestID: Bool {self._requestID != nil} /// Clears the value of `requestID`. Subsequent reads from it will return its default value. public mutating func clearRequestID() {self._requestID = nil} @@ -929,11 +1115,11 @@ public struct Netclode_V1_GitDiffRequest: Sendable { // methods supported on all messages. public var requestID: String { - get {return _requestID ?? String()} + get {_requestID ?? String()} set {_requestID = newValue} } /// Returns true if `requestID` has been explicitly set. - public var hasRequestID: Bool {return self._requestID != nil} + public var hasRequestID: Bool {self._requestID != nil} /// Clears the value of `requestID`. Subsequent reads from it will return its default value. public mutating func clearRequestID() {self._requestID = nil} @@ -941,11 +1127,11 @@ public struct Netclode_V1_GitDiffRequest: Sendable { /// Specific file path, or all files if empty public var file: String { - get {return _file ?? String()} + get {_file ?? String()} set {_file = newValue} } /// Returns true if `file` has been explicitly set. - public var hasFile: Bool {return self._file != nil} + public var hasFile: Bool {self._file != nil} /// Clears the value of `file`. Subsequent reads from it will return its default value. public mutating func clearFile() {self._file = nil} @@ -963,11 +1149,11 @@ public struct Netclode_V1_ListModelsRequest: Sendable { // methods supported on all messages. public var requestID: String { - get {return _requestID ?? String()} + get {_requestID ?? String()} set {_requestID = newValue} } /// Returns true if `requestID` has been explicitly set. - public var hasRequestID: Bool {return self._requestID != nil} + public var hasRequestID: Bool {self._requestID != nil} /// Clears the value of `requestID`. Subsequent reads from it will return its default value. public mutating func clearRequestID() {self._requestID = nil} @@ -976,20 +1162,31 @@ public struct Netclode_V1_ListModelsRequest: Sendable { /// For Copilot: which backend's models to list public var copilotBackend: Netclode_V1_CopilotBackend { - get {return _copilotBackend ?? .unspecified} + get {_copilotBackend ?? .unspecified} set {_copilotBackend = newValue} } /// Returns true if `copilotBackend` has been explicitly set. - public var hasCopilotBackend: Bool {return self._copilotBackend != nil} + public var hasCopilotBackend: Bool {self._copilotBackend != nil} /// Clears the value of `copilotBackend`. Subsequent reads from it will return its default value. public mutating func clearCopilotBackend() {self._copilotBackend = nil} + /// Hint from client to include Codex :oauth model variants + public var codexOauthAvailable: Bool { + get {_codexOauthAvailable ?? false} + set {_codexOauthAvailable = newValue} + } + /// Returns true if `codexOauthAvailable` has been explicitly set. + public var hasCodexOauthAvailable: Bool {self._codexOauthAvailable != nil} + /// Clears the value of `codexOauthAvailable`. Subsequent reads from it will return its default value. + public mutating func clearCodexOauthAvailable() {self._codexOauthAvailable = nil} + public var unknownFields = SwiftProtobuf.UnknownStorage() public init() {} fileprivate var _requestID: String? = nil fileprivate var _copilotBackend: Netclode_V1_CopilotBackend? = nil + fileprivate var _codexOauthAvailable: Bool? = nil } public struct Netclode_V1_GetCopilotStatusRequest: Sendable { @@ -998,11 +1195,11 @@ public struct Netclode_V1_GetCopilotStatusRequest: Sendable { // methods supported on all messages. public var requestID: String { - get {return _requestID ?? String()} + get {_requestID ?? String()} set {_requestID = newValue} } /// Returns true if `requestID` has been explicitly set. - public var hasRequestID: Bool {return self._requestID != nil} + public var hasRequestID: Bool {self._requestID != nil} /// Clears the value of `requestID`. Subsequent reads from it will return its default value. public mutating func clearRequestID() {self._requestID = nil} @@ -1019,11 +1216,11 @@ public struct Netclode_V1_ListSnapshotsRequest: Sendable { // methods supported on all messages. public var requestID: String { - get {return _requestID ?? String()} + get {_requestID ?? String()} set {_requestID = newValue} } /// Returns true if `requestID` has been explicitly set. - public var hasRequestID: Bool {return self._requestID != nil} + public var hasRequestID: Bool {self._requestID != nil} /// Clears the value of `requestID`. Subsequent reads from it will return its default value. public mutating func clearRequestID() {self._requestID = nil} @@ -1042,11 +1239,11 @@ public struct Netclode_V1_RestoreSnapshotRequest: Sendable { // methods supported on all messages. public var requestID: String { - get {return _requestID ?? String()} + get {_requestID ?? String()} set {_requestID = newValue} } /// Returns true if `requestID` has been explicitly set. - public var hasRequestID: Bool {return self._requestID != nil} + public var hasRequestID: Bool {self._requestID != nil} /// Clears the value of `requestID`. Subsequent reads from it will return its default value. public mutating func clearRequestID() {self._requestID = nil} @@ -1067,11 +1264,11 @@ public struct Netclode_V1_UpdateRepoAccessRequest: Sendable { // methods supported on all messages. public var requestID: String { - get {return _requestID ?? String()} + get {_requestID ?? String()} set {_requestID = newValue} } /// Returns true if `requestID` has been explicitly set. - public var hasRequestID: Bool {return self._requestID != nil} + public var hasRequestID: Bool {self._requestID != nil} /// Clears the value of `requestID`. Subsequent reads from it will return its default value. public mutating func clearRequestID() {self._requestID = nil} @@ -1093,11 +1290,74 @@ public struct Netclode_V1_GetResourceLimitsRequest: Sendable { // methods supported on all messages. public var requestID: String { - get {return _requestID ?? String()} + get {_requestID ?? String()} + set {_requestID = newValue} + } + /// Returns true if `requestID` has been explicitly set. + public var hasRequestID: Bool {self._requestID != nil} + /// Clears the value of `requestID`. Subsequent reads from it will return its default value. + public mutating func clearRequestID() {self._requestID = nil} + + public var unknownFields = SwiftProtobuf.UnknownStorage() + + public init() {} + + fileprivate var _requestID: String? = nil +} + +public struct Netclode_V1_CodexAuthStartRequest: Sendable { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + public var requestID: String { + get {_requestID ?? String()} + set {_requestID = newValue} + } + /// Returns true if `requestID` has been explicitly set. + public var hasRequestID: Bool {self._requestID != nil} + /// Clears the value of `requestID`. Subsequent reads from it will return its default value. + public mutating func clearRequestID() {self._requestID = nil} + + public var unknownFields = SwiftProtobuf.UnknownStorage() + + public init() {} + + fileprivate var _requestID: String? = nil +} + +public struct Netclode_V1_CodexAuthStatusRequest: Sendable { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + public var requestID: String { + get {_requestID ?? String()} + set {_requestID = newValue} + } + /// Returns true if `requestID` has been explicitly set. + public var hasRequestID: Bool {self._requestID != nil} + /// Clears the value of `requestID`. Subsequent reads from it will return its default value. + public mutating func clearRequestID() {self._requestID = nil} + + public var unknownFields = SwiftProtobuf.UnknownStorage() + + public init() {} + + fileprivate var _requestID: String? = nil +} + +public struct Netclode_V1_CodexAuthLogoutRequest: Sendable { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + public var requestID: String { + get {_requestID ?? String()} set {_requestID = newValue} } /// Returns true if `requestID` has been explicitly set. - public var hasRequestID: Bool {return self._requestID != nil} + public var hasRequestID: Bool {self._requestID != nil} /// Clears the value of `requestID`. Subsequent reads from it will return its default value. public mutating func clearRequestID() {self._requestID = nil} @@ -1114,21 +1374,21 @@ public struct Netclode_V1_SessionCreatedResponse: Sendable { // methods supported on all messages. public var session: Netclode_V1_Session { - get {return _session ?? Netclode_V1_Session()} + get {_session ?? Netclode_V1_Session()} set {_session = newValue} } /// Returns true if `session` has been explicitly set. - public var hasSession: Bool {return self._session != nil} + public var hasSession: Bool {self._session != nil} /// Clears the value of `session`. Subsequent reads from it will return its default value. public mutating func clearSession() {self._session = nil} /// Echoed from request for correlation public var requestID: String { - get {return _requestID ?? String()} + get {_requestID ?? String()} set {_requestID = newValue} } /// Returns true if `requestID` has been explicitly set. - public var hasRequestID: Bool {return self._requestID != nil} + public var hasRequestID: Bool {self._requestID != nil} /// Clears the value of `requestID`. Subsequent reads from it will return its default value. public mutating func clearRequestID() {self._requestID = nil} @@ -1146,11 +1406,11 @@ public struct Netclode_V1_SessionUpdatedResponse: Sendable { // methods supported on all messages. public var session: Netclode_V1_Session { - get {return _session ?? Netclode_V1_Session()} + get {_session ?? Netclode_V1_Session()} set {_session = newValue} } /// Returns true if `session` has been explicitly set. - public var hasSession: Bool {return self._session != nil} + public var hasSession: Bool {self._session != nil} /// Clears the value of `session`. Subsequent reads from it will return its default value. public mutating func clearSession() {self._session = nil} @@ -1169,11 +1429,11 @@ public struct Netclode_V1_SessionDeletedResponse: Sendable { public var sessionID: String = String() public var requestID: String { - get {return _requestID ?? String()} + get {_requestID ?? String()} set {_requestID = newValue} } /// Returns true if `requestID` has been explicitly set. - public var hasRequestID: Bool {return self._requestID != nil} + public var hasRequestID: Bool {self._requestID != nil} /// Clears the value of `requestID`. Subsequent reads from it will return its default value. public mutating func clearRequestID() {self._requestID = nil} @@ -1192,11 +1452,11 @@ public struct Netclode_V1_SessionsDeletedAllResponse: Sendable { public var deletedIds: [String] = [] public var requestID: String { - get {return _requestID ?? String()} + get {_requestID ?? String()} set {_requestID = newValue} } /// Returns true if `requestID` has been explicitly set. - public var hasRequestID: Bool {return self._requestID != nil} + public var hasRequestID: Bool {self._requestID != nil} /// Clears the value of `requestID`. Subsequent reads from it will return its default value. public mutating func clearRequestID() {self._requestID = nil} @@ -1215,11 +1475,11 @@ public struct Netclode_V1_SessionListResponse: Sendable { public var sessions: [Netclode_V1_Session] = [] public var requestID: String { - get {return _requestID ?? String()} + get {_requestID ?? String()} set {_requestID = newValue} } /// Returns true if `requestID` has been explicitly set. - public var hasRequestID: Bool {return self._requestID != nil} + public var hasRequestID: Bool {self._requestID != nil} /// Clears the value of `requestID`. Subsequent reads from it will return its default value. public mutating func clearRequestID() {self._requestID = nil} @@ -1236,52 +1496,52 @@ public struct Netclode_V1_SessionStateResponse: @unchecked Sendable { // methods supported on all messages. public var session: Netclode_V1_Session { - get {return _storage._session ?? Netclode_V1_Session()} + get {_storage._session ?? Netclode_V1_Session()} set {_uniqueStorage()._session = newValue} } /// Returns true if `session` has been explicitly set. - public var hasSession: Bool {return _storage._session != nil} + public var hasSession: Bool {_storage._session != nil} /// Clears the value of `session`. Subsequent reads from it will return its default value. public mutating func clearSession() {_uniqueStorage()._session = nil} /// History: partial=false entries only public var entries: [Netclode_V1_StreamEntry] { - get {return _storage._entries} + get {_storage._entries} set {_uniqueStorage()._entries = newValue} } /// true if more entries available for pagination public var hasMore_p: Bool { - get {return _storage._hasMore_p} + get {_storage._hasMore_p} set {_uniqueStorage()._hasMore_p = newValue} } /// Cursor for subscribing to real-time updates public var lastStreamID: String { - get {return _storage._lastStreamID ?? String()} + get {_storage._lastStreamID ?? String()} set {_uniqueStorage()._lastStreamID = newValue} } /// Returns true if `lastStreamID` has been explicitly set. - public var hasLastStreamID: Bool {return _storage._lastStreamID != nil} + public var hasLastStreamID: Bool {_storage._lastStreamID != nil} /// Clears the value of `lastStreamID`. Subsequent reads from it will return its default value. public mutating func clearLastStreamID() {_uniqueStorage()._lastStreamID = nil} /// Accumulated streaming state if RUNNING public var inProgress: Netclode_V1_InProgressState { - get {return _storage._inProgress ?? Netclode_V1_InProgressState()} + get {_storage._inProgress ?? Netclode_V1_InProgressState()} set {_uniqueStorage()._inProgress = newValue} } /// Returns true if `inProgress` has been explicitly set. - public var hasInProgress: Bool {return _storage._inProgress != nil} + public var hasInProgress: Bool {_storage._inProgress != nil} /// Clears the value of `inProgress`. Subsequent reads from it will return its default value. public mutating func clearInProgress() {_uniqueStorage()._inProgress = nil} public var requestID: String { - get {return _storage._requestID ?? String()} + get {_storage._requestID ?? String()} set {_uniqueStorage()._requestID = newValue} } /// Returns true if `requestID` has been explicitly set. - public var hasRequestID: Bool {return _storage._requestID != nil} + public var hasRequestID: Bool {_storage._requestID != nil} /// Clears the value of `requestID`. Subsequent reads from it will return its default value. public mutating func clearRequestID() {_uniqueStorage()._requestID = nil} @@ -1300,20 +1560,20 @@ public struct Netclode_V1_SyncResponse: Sendable { public var sessions: [Netclode_V1_SessionSummary] = [] public var serverTime: SwiftProtobuf.Google_Protobuf_Timestamp { - get {return _serverTime ?? SwiftProtobuf.Google_Protobuf_Timestamp()} + get {_serverTime ?? SwiftProtobuf.Google_Protobuf_Timestamp()} set {_serverTime = newValue} } /// Returns true if `serverTime` has been explicitly set. - public var hasServerTime: Bool {return self._serverTime != nil} + public var hasServerTime: Bool {self._serverTime != nil} /// Clears the value of `serverTime`. Subsequent reads from it will return its default value. public mutating func clearServerTime() {self._serverTime = nil} public var requestID: String { - get {return _requestID ?? String()} + get {_requestID ?? String()} set {_requestID = newValue} } /// Returns true if `requestID` has been explicitly set. - public var hasRequestID: Bool {return self._requestID != nil} + public var hasRequestID: Bool {self._requestID != nil} /// Clears the value of `requestID`. Subsequent reads from it will return its default value. public mutating func clearRequestID() {self._requestID = nil} @@ -1333,16 +1593,16 @@ public struct Netclode_V1_StreamEntryResponse: @unchecked Sendable { // methods supported on all messages. public var sessionID: String { - get {return _storage._sessionID} + get {_storage._sessionID} set {_uniqueStorage()._sessionID = newValue} } public var entry: Netclode_V1_StreamEntry { - get {return _storage._entry ?? Netclode_V1_StreamEntry()} + get {_storage._entry ?? Netclode_V1_StreamEntry()} set {_uniqueStorage()._entry = newValue} } /// Returns true if `entry` has been explicitly set. - public var hasEntry: Bool {return _storage._entry != nil} + public var hasEntry: Bool {_storage._entry != nil} /// Clears the value of `entry`. Subsequent reads from it will return its default value. public mutating func clearEntry() {_uniqueStorage()._entry = nil} @@ -1365,11 +1625,36 @@ public struct Netclode_V1_PortExposedResponse: Sendable { public var previewURL: String = String() public var requestID: String { - get {return _requestID ?? String()} + get {_requestID ?? String()} + set {_requestID = newValue} + } + /// Returns true if `requestID` has been explicitly set. + public var hasRequestID: Bool {self._requestID != nil} + /// Clears the value of `requestID`. Subsequent reads from it will return its default value. + public mutating func clearRequestID() {self._requestID = nil} + + public var unknownFields = SwiftProtobuf.UnknownStorage() + + public init() {} + + fileprivate var _requestID: String? = nil +} + +public struct Netclode_V1_PortUnexposedResponse: Sendable { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + public var sessionID: String = String() + + public var port: Int32 = 0 + + public var requestID: String { + get {_requestID ?? String()} set {_requestID = newValue} } /// Returns true if `requestID` has been explicitly set. - public var hasRequestID: Bool {return self._requestID != nil} + public var hasRequestID: Bool {self._requestID != nil} /// Clears the value of `requestID`. Subsequent reads from it will return its default value. public mutating func clearRequestID() {self._requestID = nil} @@ -1388,11 +1673,11 @@ public struct Netclode_V1_GitHubReposResponse: Sendable { public var repos: [Netclode_V1_GitHubRepo] = [] public var requestID: String { - get {return _requestID ?? String()} + get {_requestID ?? String()} set {_requestID = newValue} } /// Returns true if `requestID` has been explicitly set. - public var hasRequestID: Bool {return self._requestID != nil} + public var hasRequestID: Bool {self._requestID != nil} /// Clears the value of `requestID`. Subsequent reads from it will return its default value. public mutating func clearRequestID() {self._requestID = nil} @@ -1413,11 +1698,11 @@ public struct Netclode_V1_GitStatusResponse: Sendable { public var files: [Netclode_V1_GitFileChange] = [] public var requestID: String { - get {return _requestID ?? String()} + get {_requestID ?? String()} set {_requestID = newValue} } /// Returns true if `requestID` has been explicitly set. - public var hasRequestID: Bool {return self._requestID != nil} + public var hasRequestID: Bool {self._requestID != nil} /// Clears the value of `requestID`. Subsequent reads from it will return its default value. public mutating func clearRequestID() {self._requestID = nil} @@ -1438,11 +1723,11 @@ public struct Netclode_V1_GitDiffResponse: Sendable { public var diff: String = String() public var requestID: String { - get {return _requestID ?? String()} + get {_requestID ?? String()} set {_requestID = newValue} } /// Returns true if `requestID` has been explicitly set. - public var hasRequestID: Bool {return self._requestID != nil} + public var hasRequestID: Bool {self._requestID != nil} /// Clears the value of `requestID`. Subsequent reads from it will return its default value. public mutating func clearRequestID() {self._requestID = nil} @@ -1462,21 +1747,21 @@ public struct Netclode_V1_ErrorResponse: Sendable { /// Structured error details public var error: Netclode_V1_Error { - get {return _error ?? Netclode_V1_Error()} + get {_error ?? Netclode_V1_Error()} set {_error = newValue} } /// Returns true if `error` has been explicitly set. - public var hasError: Bool {return self._error != nil} + public var hasError: Bool {self._error != nil} /// Clears the value of `error`. Subsequent reads from it will return its default value. public mutating func clearError() {self._error = nil} /// Echoed from request for correlation public var requestID: String { - get {return _requestID ?? String()} + get {_requestID ?? String()} set {_requestID = newValue} } /// Returns true if `requestID` has been explicitly set. - public var hasRequestID: Bool {return self._requestID != nil} + public var hasRequestID: Bool {self._requestID != nil} /// Clears the value of `requestID`. Subsequent reads from it will return its default value. public mutating func clearRequestID() {self._requestID = nil} @@ -1497,21 +1782,21 @@ public struct Netclode_V1_ModelsResponse: Sendable { public var models: [Netclode_V1_ModelInfo] = [] public var requestID: String { - get {return _requestID ?? String()} + get {_requestID ?? String()} set {_requestID = newValue} } /// Returns true if `requestID` has been explicitly set. - public var hasRequestID: Bool {return self._requestID != nil} + public var hasRequestID: Bool {self._requestID != nil} /// Clears the value of `requestID`. Subsequent reads from it will return its default value. public mutating func clearRequestID() {self._requestID = nil} /// Which SDK these models are for public var sdkType: Netclode_V1_SdkType { - get {return _sdkType ?? .unspecified} + get {_sdkType ?? .unspecified} set {_sdkType = newValue} } /// Returns true if `sdkType` has been explicitly set. - public var hasSdkType: Bool {return self._sdkType != nil} + public var hasSdkType: Bool {self._sdkType != nil} /// Clears the value of `sdkType`. Subsequent reads from it will return its default value. public mutating func clearSdkType() {self._sdkType = nil} @@ -1530,30 +1815,30 @@ public struct Netclode_V1_CopilotStatusResponse: Sendable { /// GitHub Copilot authentication status public var auth: Netclode_V1_CopilotAuthStatus { - get {return _auth ?? Netclode_V1_CopilotAuthStatus()} + get {_auth ?? Netclode_V1_CopilotAuthStatus()} set {_auth = newValue} } /// Returns true if `auth` has been explicitly set. - public var hasAuth: Bool {return self._auth != nil} + public var hasAuth: Bool {self._auth != nil} /// Clears the value of `auth`. Subsequent reads from it will return its default value. public mutating func clearAuth() {self._auth = nil} /// Premium request quota (only if authenticated) public var quota: Netclode_V1_CopilotPremiumQuota { - get {return _quota ?? Netclode_V1_CopilotPremiumQuota()} + get {_quota ?? Netclode_V1_CopilotPremiumQuota()} set {_quota = newValue} } /// Returns true if `quota` has been explicitly set. - public var hasQuota: Bool {return self._quota != nil} + public var hasQuota: Bool {self._quota != nil} /// Clears the value of `quota`. Subsequent reads from it will return its default value. public mutating func clearQuota() {self._quota = nil} public var requestID: String { - get {return _requestID ?? String()} + get {_requestID ?? String()} set {_requestID = newValue} } /// Returns true if `requestID` has been explicitly set. - public var hasRequestID: Bool {return self._requestID != nil} + public var hasRequestID: Bool {self._requestID != nil} /// Clears the value of `requestID`. Subsequent reads from it will return its default value. public mutating func clearRequestID() {self._requestID = nil} @@ -1566,6 +1851,127 @@ public struct Netclode_V1_CopilotStatusResponse: Sendable { fileprivate var _requestID: String? = nil } +public struct Netclode_V1_CodexAuthStartedResponse: Sendable { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + public var verificationUri: String = String() + + public var verificationUriComplete: String { + get {_verificationUriComplete ?? String()} + set {_verificationUriComplete = newValue} + } + /// Returns true if `verificationUriComplete` has been explicitly set. + public var hasVerificationUriComplete: Bool {self._verificationUriComplete != nil} + /// Clears the value of `verificationUriComplete`. Subsequent reads from it will return its default value. + public mutating func clearVerificationUriComplete() {self._verificationUriComplete = nil} + + public var userCode: String = String() + + public var intervalSeconds: Int32 = 0 + + public var expiresAt: SwiftProtobuf.Google_Protobuf_Timestamp { + get {_expiresAt ?? SwiftProtobuf.Google_Protobuf_Timestamp()} + set {_expiresAt = newValue} + } + /// Returns true if `expiresAt` has been explicitly set. + public var hasExpiresAt: Bool {self._expiresAt != nil} + /// Clears the value of `expiresAt`. Subsequent reads from it will return its default value. + public mutating func clearExpiresAt() {self._expiresAt = nil} + + public var requestID: String { + get {_requestID ?? String()} + set {_requestID = newValue} + } + /// Returns true if `requestID` has been explicitly set. + public var hasRequestID: Bool {self._requestID != nil} + /// Clears the value of `requestID`. Subsequent reads from it will return its default value. + public mutating func clearRequestID() {self._requestID = nil} + + public var unknownFields = SwiftProtobuf.UnknownStorage() + + public init() {} + + fileprivate var _verificationUriComplete: String? = nil + fileprivate var _expiresAt: SwiftProtobuf.Google_Protobuf_Timestamp? = nil + fileprivate var _requestID: String? = nil +} + +public struct Netclode_V1_CodexAuthStatusResponse: Sendable { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + public var state: Netclode_V1_CodexAuthState = .unspecified + + public var accountID: String { + get {_accountID ?? String()} + set {_accountID = newValue} + } + /// Returns true if `accountID` has been explicitly set. + public var hasAccountID: Bool {self._accountID != nil} + /// Clears the value of `accountID`. Subsequent reads from it will return its default value. + public mutating func clearAccountID() {self._accountID = nil} + + public var expiresAt: SwiftProtobuf.Google_Protobuf_Timestamp { + get {_expiresAt ?? SwiftProtobuf.Google_Protobuf_Timestamp()} + set {_expiresAt = newValue} + } + /// Returns true if `expiresAt` has been explicitly set. + public var hasExpiresAt: Bool {self._expiresAt != nil} + /// Clears the value of `expiresAt`. Subsequent reads from it will return its default value. + public mutating func clearExpiresAt() {self._expiresAt = nil} + + public var error: String { + get {_error ?? String()} + set {_error = newValue} + } + /// Returns true if `error` has been explicitly set. + public var hasError: Bool {self._error != nil} + /// Clears the value of `error`. Subsequent reads from it will return its default value. + public mutating func clearError() {self._error = nil} + + public var requestID: String { + get {_requestID ?? String()} + set {_requestID = newValue} + } + /// Returns true if `requestID` has been explicitly set. + public var hasRequestID: Bool {self._requestID != nil} + /// Clears the value of `requestID`. Subsequent reads from it will return its default value. + public mutating func clearRequestID() {self._requestID = nil} + + public var unknownFields = SwiftProtobuf.UnknownStorage() + + public init() {} + + fileprivate var _accountID: String? = nil + fileprivate var _expiresAt: SwiftProtobuf.Google_Protobuf_Timestamp? = nil + fileprivate var _error: String? = nil + fileprivate var _requestID: String? = nil +} + +public struct Netclode_V1_CodexAuthLoggedOutResponse: Sendable { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + public var requestID: String { + get {_requestID ?? String()} + set {_requestID = newValue} + } + /// Returns true if `requestID` has been explicitly set. + public var hasRequestID: Bool {self._requestID != nil} + /// Clears the value of `requestID`. Subsequent reads from it will return its default value. + public mutating func clearRequestID() {self._requestID = nil} + + public var unknownFields = SwiftProtobuf.UnknownStorage() + + public init() {} + + fileprivate var _requestID: String? = nil +} + /// SnapshotCreatedResponse is pushed to clients when an auto-snapshot is created after a turn. public struct Netclode_V1_SnapshotCreatedResponse: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the @@ -1575,11 +1981,11 @@ public struct Netclode_V1_SnapshotCreatedResponse: Sendable { public var sessionID: String = String() public var snapshot: Netclode_V1_Snapshot { - get {return _snapshot ?? Netclode_V1_Snapshot()} + get {_snapshot ?? Netclode_V1_Snapshot()} set {_snapshot = newValue} } /// Returns true if `snapshot` has been explicitly set. - public var hasSnapshot: Bool {return self._snapshot != nil} + public var hasSnapshot: Bool {self._snapshot != nil} /// Clears the value of `snapshot`. Subsequent reads from it will return its default value. public mutating func clearSnapshot() {self._snapshot = nil} @@ -1601,11 +2007,11 @@ public struct Netclode_V1_SnapshotListResponse: Sendable { public var snapshots: [Netclode_V1_Snapshot] = [] public var requestID: String { - get {return _requestID ?? String()} + get {_requestID ?? String()} set {_requestID = newValue} } /// Returns true if `requestID` has been explicitly set. - public var hasRequestID: Bool {return self._requestID != nil} + public var hasRequestID: Bool {self._requestID != nil} /// Clears the value of `requestID`. Subsequent reads from it will return its default value. public mutating func clearRequestID() {self._requestID = nil} @@ -1630,11 +2036,11 @@ public struct Netclode_V1_SnapshotRestoredResponse: Sendable { public var messagesRestored: Int32 = 0 public var requestID: String { - get {return _requestID ?? String()} + get {_requestID ?? String()} set {_requestID = newValue} } /// Returns true if `requestID` has been explicitly set. - public var hasRequestID: Bool {return self._requestID != nil} + public var hasRequestID: Bool {self._requestID != nil} /// Clears the value of `requestID`. Subsequent reads from it will return its default value. public mutating func clearRequestID() {self._requestID = nil} @@ -1657,11 +2063,11 @@ public struct Netclode_V1_RepoAccessUpdatedResponse: Sendable { public var repoAccess: Netclode_V1_RepoAccess = .unspecified public var requestID: String { - get {return _requestID ?? String()} + get {_requestID ?? String()} set {_requestID = newValue} } /// Returns true if `requestID` has been explicitly set. - public var hasRequestID: Bool {return self._requestID != nil} + public var hasRequestID: Bool {self._requestID != nil} /// Clears the value of `requestID`. Subsequent reads from it will return its default value. public mutating func clearRequestID() {self._requestID = nil} @@ -1692,11 +2098,11 @@ public struct Netclode_V1_ResourceLimitsResponse: Sendable { public var defaultMemoryMb: Int32 = 0 public var requestID: String { - get {return _requestID ?? String()} + get {_requestID ?? String()} set {_requestID = newValue} } /// Returns true if `requestID` has been explicitly set. - public var hasRequestID: Bool {return self._requestID != nil} + public var hasRequestID: Bool {self._requestID != nil} /// Clears the value of `requestID`. Subsequent reads from it will return its default value. public mutating func clearRequestID() {self._requestID = nil} @@ -1711,9 +2117,13 @@ public struct Netclode_V1_ResourceLimitsResponse: Sendable { fileprivate let _protobuf_package = "netclode.v1" +extension Netclode_V1_CodexAuthState: SwiftProtobuf._ProtoNameProviding { + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{2}\0CODEX_AUTH_STATE_UNSPECIFIED\0\u{1}CODEX_AUTH_STATE_UNAUTHENTICATED\0\u{1}CODEX_AUTH_STATE_PENDING\0\u{1}CODEX_AUTH_STATE_READY\0\u{1}CODEX_AUTH_STATE_ERROR\0") +} + extension Netclode_V1_ClientMessage: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = _protobuf_package + ".ClientMessage" - public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}create_session\0\u{3}list_sessions\0\u{3}open_session\0\u{3}resume_session\0\u{3}pause_session\0\u{3}delete_session\0\u{3}delete_all_sessions\0\u{3}send_prompt\0\u{3}interrupt_prompt\0\u{3}terminal_input\0\u{3}terminal_resize\0\u{3}expose_port\0\u{1}sync\0\u{3}list_github_repos\0\u{3}git_status\0\u{3}git_diff\0\u{3}list_models\0\u{3}get_copilot_status\0\u{3}list_snapshots\0\u{3}restore_snapshot\0\u{3}update_repo_access\0\u{3}get_resource_limits\0") + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}create_session\0\u{3}list_sessions\0\u{3}open_session\0\u{3}resume_session\0\u{3}pause_session\0\u{3}delete_session\0\u{3}delete_all_sessions\0\u{3}send_prompt\0\u{3}interrupt_prompt\0\u{3}terminal_input\0\u{3}terminal_resize\0\u{3}expose_port\0\u{1}sync\0\u{3}list_github_repos\0\u{3}git_status\0\u{3}git_diff\0\u{3}list_models\0\u{3}get_copilot_status\0\u{3}list_snapshots\0\u{3}restore_snapshot\0\u{3}update_repo_access\0\u{3}get_resource_limits\0\u{3}codex_auth_start\0\u{3}codex_auth_status\0\u{3}codex_auth_logout\0\u{3}unexpose_port\0") public mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { @@ -2007,36 +2417,88 @@ extension Netclode_V1_ClientMessage: SwiftProtobuf.Message, SwiftProtobuf._Messa self.message = .getResourceLimits(v) } }() - default: break - } - } - } - - public func traverse(visitor: inout V) throws { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every if/case branch local when no optimizations - // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and - // https://github.com/apple/swift-protobuf/issues/1182 - switch self.message { - case .createSession?: try { - guard case .createSession(let v)? = self.message else { preconditionFailure() } - try visitor.visitSingularMessageField(value: v, fieldNumber: 1) - }() - case .listSessions?: try { - guard case .listSessions(let v)? = self.message else { preconditionFailure() } - try visitor.visitSingularMessageField(value: v, fieldNumber: 2) - }() - case .openSession?: try { - guard case .openSession(let v)? = self.message else { preconditionFailure() } - try visitor.visitSingularMessageField(value: v, fieldNumber: 3) - }() - case .resumeSession?: try { - guard case .resumeSession(let v)? = self.message else { preconditionFailure() } - try visitor.visitSingularMessageField(value: v, fieldNumber: 4) - }() - case .pauseSession?: try { - guard case .pauseSession(let v)? = self.message else { preconditionFailure() } - try visitor.visitSingularMessageField(value: v, fieldNumber: 5) + case 23: try { + var v: Netclode_V1_CodexAuthStartRequest? + var hadOneofValue = false + if let current = self.message { + hadOneofValue = true + if case .codexAuthStart(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.message = .codexAuthStart(v) + } + }() + case 24: try { + var v: Netclode_V1_CodexAuthStatusRequest? + var hadOneofValue = false + if let current = self.message { + hadOneofValue = true + if case .codexAuthStatus(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.message = .codexAuthStatus(v) + } + }() + case 25: try { + var v: Netclode_V1_CodexAuthLogoutRequest? + var hadOneofValue = false + if let current = self.message { + hadOneofValue = true + if case .codexAuthLogout(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.message = .codexAuthLogout(v) + } + }() + case 26: try { + var v: Netclode_V1_UnexposePortRequest? + var hadOneofValue = false + if let current = self.message { + hadOneofValue = true + if case .unexposePort(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.message = .unexposePort(v) + } + }() + default: break + } + } + } + + public func traverse(visitor: inout V) throws { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 + switch self.message { + case .createSession?: try { + guard case .createSession(let v)? = self.message else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 1) + }() + case .listSessions?: try { + guard case .listSessions(let v)? = self.message else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 2) + }() + case .openSession?: try { + guard case .openSession(let v)? = self.message else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 3) + }() + case .resumeSession?: try { + guard case .resumeSession(let v)? = self.message else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 4) + }() + case .pauseSession?: try { + guard case .pauseSession(let v)? = self.message else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 5) }() case .deleteSession?: try { guard case .deleteSession(let v)? = self.message else { preconditionFailure() } @@ -2106,6 +2568,22 @@ extension Netclode_V1_ClientMessage: SwiftProtobuf.Message, SwiftProtobuf._Messa guard case .getResourceLimits(let v)? = self.message else { preconditionFailure() } try visitor.visitSingularMessageField(value: v, fieldNumber: 22) }() + case .codexAuthStart?: try { + guard case .codexAuthStart(let v)? = self.message else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 23) + }() + case .codexAuthStatus?: try { + guard case .codexAuthStatus(let v)? = self.message else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 24) + }() + case .codexAuthLogout?: try { + guard case .codexAuthLogout(let v)? = self.message else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 25) + }() + case .unexposePort?: try { + guard case .unexposePort(let v)? = self.message else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 26) + }() case nil: break } try unknownFields.traverse(visitor: &visitor) @@ -2120,7 +2598,7 @@ extension Netclode_V1_ClientMessage: SwiftProtobuf.Message, SwiftProtobuf._Messa extension Netclode_V1_ServerMessage: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = _protobuf_package + ".ServerMessage" - public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}session_created\0\u{3}session_updated\0\u{3}session_deleted\0\u{3}sessions_deleted_all\0\u{3}session_list\0\u{3}session_state\0\u{3}sync_response\0\u{3}stream_entry\0\u{4}\u{5}port_exposed\0\u{3}github_repos\0\u{3}git_status\0\u{3}git_diff\0\u{1}error\0\u{1}models\0\u{3}copilot_status\0\u{3}snapshot_created\0\u{3}snapshot_list\0\u{3}snapshot_restored\0\u{3}repo_access_updated\0\u{3}resource_limits\0") + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}session_created\0\u{3}session_updated\0\u{3}session_deleted\0\u{3}sessions_deleted_all\0\u{3}session_list\0\u{3}session_state\0\u{3}sync_response\0\u{3}stream_entry\0\u{4}\u{5}port_exposed\0\u{3}github_repos\0\u{3}git_status\0\u{3}git_diff\0\u{1}error\0\u{1}models\0\u{3}copilot_status\0\u{3}snapshot_created\0\u{3}snapshot_list\0\u{3}snapshot_restored\0\u{3}repo_access_updated\0\u{3}resource_limits\0\u{3}codex_auth_started\0\u{3}codex_auth_status\0\u{3}codex_auth_logged_out\0\u{3}port_unexposed\0") public mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { @@ -2388,6 +2866,58 @@ extension Netclode_V1_ServerMessage: SwiftProtobuf.Message, SwiftProtobuf._Messa self.message = .resourceLimits(v) } }() + case 25: try { + var v: Netclode_V1_CodexAuthStartedResponse? + var hadOneofValue = false + if let current = self.message { + hadOneofValue = true + if case .codexAuthStarted(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.message = .codexAuthStarted(v) + } + }() + case 26: try { + var v: Netclode_V1_CodexAuthStatusResponse? + var hadOneofValue = false + if let current = self.message { + hadOneofValue = true + if case .codexAuthStatus(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.message = .codexAuthStatus(v) + } + }() + case 27: try { + var v: Netclode_V1_CodexAuthLoggedOutResponse? + var hadOneofValue = false + if let current = self.message { + hadOneofValue = true + if case .codexAuthLoggedOut(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.message = .codexAuthLoggedOut(v) + } + }() + case 28: try { + var v: Netclode_V1_PortUnexposedResponse? + var hadOneofValue = false + if let current = self.message { + hadOneofValue = true + if case .portUnexposed(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.message = .portUnexposed(v) + } + }() default: break } } @@ -2479,6 +3009,22 @@ extension Netclode_V1_ServerMessage: SwiftProtobuf.Message, SwiftProtobuf._Messa guard case .resourceLimits(let v)? = self.message else { preconditionFailure() } try visitor.visitSingularMessageField(value: v, fieldNumber: 24) }() + case .codexAuthStarted?: try { + guard case .codexAuthStarted(let v)? = self.message else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 25) + }() + case .codexAuthStatus?: try { + guard case .codexAuthStatus(let v)? = self.message else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 26) + }() + case .codexAuthLoggedOut?: try { + guard case .codexAuthLoggedOut(let v)? = self.message else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 27) + }() + case .portUnexposed?: try { + guard case .portUnexposed(let v)? = self.message else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 28) + }() case nil: break } try unknownFields.traverse(visitor: &visitor) @@ -2521,9 +3067,58 @@ extension Netclode_V1_NetworkConfig: SwiftProtobuf.Message, SwiftProtobuf._Messa } } +extension Netclode_V1_CodexOAuthTokens: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + public static let protoMessageName: String = _protobuf_package + ".CodexOAuthTokens" + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}access_token\0\u{3}id_token\0\u{3}refresh_token\0\u{3}expires_at\0") + + public mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularStringField(value: &self.accessToken) }() + case 2: try { try decoder.decodeSingularStringField(value: &self.idToken) }() + case 3: try { try decoder.decodeSingularStringField(value: &self.refreshToken) }() + case 4: try { try decoder.decodeSingularMessageField(value: &self._expiresAt) }() + default: break + } + } + } + + public func traverse(visitor: inout V) throws { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 + if !self.accessToken.isEmpty { + try visitor.visitSingularStringField(value: self.accessToken, fieldNumber: 1) + } + if !self.idToken.isEmpty { + try visitor.visitSingularStringField(value: self.idToken, fieldNumber: 2) + } + if !self.refreshToken.isEmpty { + try visitor.visitSingularStringField(value: self.refreshToken, fieldNumber: 3) + } + try { if let v = self._expiresAt { + try visitor.visitSingularMessageField(value: v, fieldNumber: 4) + } }() + try unknownFields.traverse(visitor: &visitor) + } + + public static func ==(lhs: Netclode_V1_CodexOAuthTokens, rhs: Netclode_V1_CodexOAuthTokens) -> Bool { + if lhs.accessToken != rhs.accessToken {return false} + if lhs.idToken != rhs.idToken {return false} + if lhs.refreshToken != rhs.refreshToken {return false} + if lhs._expiresAt != rhs._expiresAt {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + extension Netclode_V1_CreateSessionRequest: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = _protobuf_package + ".CreateSessionRequest" - public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}request_id\0\u{1}name\0\u{1}repos\0\u{3}repo_access\0\u{3}initial_prompt\0\u{3}sdk_type\0\u{1}model\0\u{3}copilot_backend\0\u{3}network_config\0\u{1}resources\0") + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}request_id\0\u{1}name\0\u{1}repos\0\u{3}repo_access\0\u{3}initial_prompt\0\u{3}sdk_type\0\u{1}model\0\u{3}copilot_backend\0\u{3}network_config\0\u{1}resources\0\u{3}codex_oauth_tokens\0") public mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { @@ -2541,6 +3136,7 @@ extension Netclode_V1_CreateSessionRequest: SwiftProtobuf.Message, SwiftProtobuf case 8: try { try decoder.decodeSingularEnumField(value: &self._copilotBackend) }() case 9: try { try decoder.decodeSingularMessageField(value: &self._networkConfig) }() case 10: try { try decoder.decodeSingularMessageField(value: &self._resources) }() + case 11: try { try decoder.decodeSingularMessageField(value: &self._codexOauthTokens) }() default: break } } @@ -2581,6 +3177,9 @@ extension Netclode_V1_CreateSessionRequest: SwiftProtobuf.Message, SwiftProtobuf try { if let v = self._resources { try visitor.visitSingularMessageField(value: v, fieldNumber: 10) } }() + try { if let v = self._codexOauthTokens { + try visitor.visitSingularMessageField(value: v, fieldNumber: 11) + } }() try unknownFields.traverse(visitor: &visitor) } @@ -2595,6 +3194,7 @@ extension Netclode_V1_CreateSessionRequest: SwiftProtobuf.Message, SwiftProtobuf if lhs._copilotBackend != rhs._copilotBackend {return false} if lhs._networkConfig != rhs._networkConfig {return false} if lhs._resources != rhs._resources {return false} + if lhs._codexOauthTokens != rhs._codexOauthTokens {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } @@ -3054,6 +3654,50 @@ extension Netclode_V1_ExposePortRequest: SwiftProtobuf.Message, SwiftProtobuf._M } } +extension Netclode_V1_UnexposePortRequest: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + public static let protoMessageName: String = _protobuf_package + ".UnexposePortRequest" + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}request_id\0\u{3}session_id\0\u{1}port\0") + + public mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularStringField(value: &self._requestID) }() + case 2: try { try decoder.decodeSingularStringField(value: &self.sessionID) }() + case 3: try { try decoder.decodeSingularInt32Field(value: &self.port) }() + default: break + } + } + } + + public func traverse(visitor: inout V) throws { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 + try { if let v = self._requestID { + try visitor.visitSingularStringField(value: v, fieldNumber: 1) + } }() + if !self.sessionID.isEmpty { + try visitor.visitSingularStringField(value: self.sessionID, fieldNumber: 2) + } + if self.port != 0 { + try visitor.visitSingularInt32Field(value: self.port, fieldNumber: 3) + } + try unknownFields.traverse(visitor: &visitor) + } + + public static func ==(lhs: Netclode_V1_UnexposePortRequest, rhs: Netclode_V1_UnexposePortRequest) -> Bool { + if lhs._requestID != rhs._requestID {return false} + if lhs.sessionID != rhs.sessionID {return false} + if lhs.port != rhs.port {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + extension Netclode_V1_SyncRequest: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = _protobuf_package + ".SyncRequest" public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}request_id\0") @@ -3207,7 +3851,7 @@ extension Netclode_V1_GitDiffRequest: SwiftProtobuf.Message, SwiftProtobuf._Mess extension Netclode_V1_ListModelsRequest: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = _protobuf_package + ".ListModelsRequest" - public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}request_id\0\u{3}sdk_type\0\u{3}copilot_backend\0") + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}request_id\0\u{3}sdk_type\0\u{3}copilot_backend\0\u{3}codex_oauth_available\0") public mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { @@ -3218,6 +3862,7 @@ extension Netclode_V1_ListModelsRequest: SwiftProtobuf.Message, SwiftProtobuf._M case 1: try { try decoder.decodeSingularStringField(value: &self._requestID) }() case 2: try { try decoder.decodeSingularEnumField(value: &self.sdkType) }() case 3: try { try decoder.decodeSingularEnumField(value: &self._copilotBackend) }() + case 4: try { try decoder.decodeSingularBoolField(value: &self._codexOauthAvailable) }() default: break } } @@ -3237,6 +3882,9 @@ extension Netclode_V1_ListModelsRequest: SwiftProtobuf.Message, SwiftProtobuf._M try { if let v = self._copilotBackend { try visitor.visitSingularEnumField(value: v, fieldNumber: 3) } }() + try { if let v = self._codexOauthAvailable { + try visitor.visitSingularBoolField(value: v, fieldNumber: 4) + } }() try unknownFields.traverse(visitor: &visitor) } @@ -3244,6 +3892,7 @@ extension Netclode_V1_ListModelsRequest: SwiftProtobuf.Message, SwiftProtobuf._M if lhs._requestID != rhs._requestID {return false} if lhs.sdkType != rhs.sdkType {return false} if lhs._copilotBackend != rhs._copilotBackend {return false} + if lhs._codexOauthAvailable != rhs._codexOauthAvailable {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } @@ -3444,6 +4093,108 @@ extension Netclode_V1_GetResourceLimitsRequest: SwiftProtobuf.Message, SwiftProt } } +extension Netclode_V1_CodexAuthStartRequest: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + public static let protoMessageName: String = _protobuf_package + ".CodexAuthStartRequest" + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}request_id\0") + + public mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularStringField(value: &self._requestID) }() + default: break + } + } + } + + public func traverse(visitor: inout V) throws { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 + try { if let v = self._requestID { + try visitor.visitSingularStringField(value: v, fieldNumber: 1) + } }() + try unknownFields.traverse(visitor: &visitor) + } + + public static func ==(lhs: Netclode_V1_CodexAuthStartRequest, rhs: Netclode_V1_CodexAuthStartRequest) -> Bool { + if lhs._requestID != rhs._requestID {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Netclode_V1_CodexAuthStatusRequest: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + public static let protoMessageName: String = _protobuf_package + ".CodexAuthStatusRequest" + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}request_id\0") + + public mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularStringField(value: &self._requestID) }() + default: break + } + } + } + + public func traverse(visitor: inout V) throws { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 + try { if let v = self._requestID { + try visitor.visitSingularStringField(value: v, fieldNumber: 1) + } }() + try unknownFields.traverse(visitor: &visitor) + } + + public static func ==(lhs: Netclode_V1_CodexAuthStatusRequest, rhs: Netclode_V1_CodexAuthStatusRequest) -> Bool { + if lhs._requestID != rhs._requestID {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Netclode_V1_CodexAuthLogoutRequest: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + public static let protoMessageName: String = _protobuf_package + ".CodexAuthLogoutRequest" + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}request_id\0") + + public mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularStringField(value: &self._requestID) }() + default: break + } + } + } + + public func traverse(visitor: inout V) throws { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 + try { if let v = self._requestID { + try visitor.visitSingularStringField(value: v, fieldNumber: 1) + } }() + try unknownFields.traverse(visitor: &visitor) + } + + public static func ==(lhs: Netclode_V1_CodexAuthLogoutRequest, rhs: Netclode_V1_CodexAuthLogoutRequest) -> Bool { + if lhs._requestID != rhs._requestID {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + extension Netclode_V1_SessionCreatedResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = _protobuf_package + ".SessionCreatedResponse" public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}session\0\u{3}request_id\0") @@ -3909,6 +4660,50 @@ extension Netclode_V1_PortExposedResponse: SwiftProtobuf.Message, SwiftProtobuf. } } +extension Netclode_V1_PortUnexposedResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + public static let protoMessageName: String = _protobuf_package + ".PortUnexposedResponse" + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}session_id\0\u{1}port\0\u{3}request_id\0") + + public mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularStringField(value: &self.sessionID) }() + case 2: try { try decoder.decodeSingularInt32Field(value: &self.port) }() + case 3: try { try decoder.decodeSingularStringField(value: &self._requestID) }() + default: break + } + } + } + + public func traverse(visitor: inout V) throws { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 + if !self.sessionID.isEmpty { + try visitor.visitSingularStringField(value: self.sessionID, fieldNumber: 1) + } + if self.port != 0 { + try visitor.visitSingularInt32Field(value: self.port, fieldNumber: 2) + } + try { if let v = self._requestID { + try visitor.visitSingularStringField(value: v, fieldNumber: 3) + } }() + try unknownFields.traverse(visitor: &visitor) + } + + public static func ==(lhs: Netclode_V1_PortUnexposedResponse, rhs: Netclode_V1_PortUnexposedResponse) -> Bool { + if lhs.sessionID != rhs.sessionID {return false} + if lhs.port != rhs.port {return false} + if lhs._requestID != rhs._requestID {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + extension Netclode_V1_GitHubReposResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = _protobuf_package + ".GitHubReposResponse" public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}repos\0\u{3}request_id\0") @@ -4163,6 +4958,153 @@ extension Netclode_V1_CopilotStatusResponse: SwiftProtobuf.Message, SwiftProtobu } } +extension Netclode_V1_CodexAuthStartedResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + public static let protoMessageName: String = _protobuf_package + ".CodexAuthStartedResponse" + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}verification_uri\0\u{3}verification_uri_complete\0\u{3}user_code\0\u{3}interval_seconds\0\u{3}expires_at\0\u{3}request_id\0") + + public mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularStringField(value: &self.verificationUri) }() + case 2: try { try decoder.decodeSingularStringField(value: &self._verificationUriComplete) }() + case 3: try { try decoder.decodeSingularStringField(value: &self.userCode) }() + case 4: try { try decoder.decodeSingularInt32Field(value: &self.intervalSeconds) }() + case 5: try { try decoder.decodeSingularMessageField(value: &self._expiresAt) }() + case 6: try { try decoder.decodeSingularStringField(value: &self._requestID) }() + default: break + } + } + } + + public func traverse(visitor: inout V) throws { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 + if !self.verificationUri.isEmpty { + try visitor.visitSingularStringField(value: self.verificationUri, fieldNumber: 1) + } + try { if let v = self._verificationUriComplete { + try visitor.visitSingularStringField(value: v, fieldNumber: 2) + } }() + if !self.userCode.isEmpty { + try visitor.visitSingularStringField(value: self.userCode, fieldNumber: 3) + } + if self.intervalSeconds != 0 { + try visitor.visitSingularInt32Field(value: self.intervalSeconds, fieldNumber: 4) + } + try { if let v = self._expiresAt { + try visitor.visitSingularMessageField(value: v, fieldNumber: 5) + } }() + try { if let v = self._requestID { + try visitor.visitSingularStringField(value: v, fieldNumber: 6) + } }() + try unknownFields.traverse(visitor: &visitor) + } + + public static func ==(lhs: Netclode_V1_CodexAuthStartedResponse, rhs: Netclode_V1_CodexAuthStartedResponse) -> Bool { + if lhs.verificationUri != rhs.verificationUri {return false} + if lhs._verificationUriComplete != rhs._verificationUriComplete {return false} + if lhs.userCode != rhs.userCode {return false} + if lhs.intervalSeconds != rhs.intervalSeconds {return false} + if lhs._expiresAt != rhs._expiresAt {return false} + if lhs._requestID != rhs._requestID {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Netclode_V1_CodexAuthStatusResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + public static let protoMessageName: String = _protobuf_package + ".CodexAuthStatusResponse" + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}state\0\u{3}account_id\0\u{3}expires_at\0\u{1}error\0\u{3}request_id\0") + + public mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularEnumField(value: &self.state) }() + case 2: try { try decoder.decodeSingularStringField(value: &self._accountID) }() + case 3: try { try decoder.decodeSingularMessageField(value: &self._expiresAt) }() + case 4: try { try decoder.decodeSingularStringField(value: &self._error) }() + case 5: try { try decoder.decodeSingularStringField(value: &self._requestID) }() + default: break + } + } + } + + public func traverse(visitor: inout V) throws { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 + if self.state != .unspecified { + try visitor.visitSingularEnumField(value: self.state, fieldNumber: 1) + } + try { if let v = self._accountID { + try visitor.visitSingularStringField(value: v, fieldNumber: 2) + } }() + try { if let v = self._expiresAt { + try visitor.visitSingularMessageField(value: v, fieldNumber: 3) + } }() + try { if let v = self._error { + try visitor.visitSingularStringField(value: v, fieldNumber: 4) + } }() + try { if let v = self._requestID { + try visitor.visitSingularStringField(value: v, fieldNumber: 5) + } }() + try unknownFields.traverse(visitor: &visitor) + } + + public static func ==(lhs: Netclode_V1_CodexAuthStatusResponse, rhs: Netclode_V1_CodexAuthStatusResponse) -> Bool { + if lhs.state != rhs.state {return false} + if lhs._accountID != rhs._accountID {return false} + if lhs._expiresAt != rhs._expiresAt {return false} + if lhs._error != rhs._error {return false} + if lhs._requestID != rhs._requestID {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Netclode_V1_CodexAuthLoggedOutResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + public static let protoMessageName: String = _protobuf_package + ".CodexAuthLoggedOutResponse" + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}request_id\0") + + public mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularStringField(value: &self._requestID) }() + default: break + } + } + } + + public func traverse(visitor: inout V) throws { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 + try { if let v = self._requestID { + try visitor.visitSingularStringField(value: v, fieldNumber: 1) + } }() + try unknownFields.traverse(visitor: &visitor) + } + + public static func ==(lhs: Netclode_V1_CodexAuthLoggedOutResponse, rhs: Netclode_V1_CodexAuthLoggedOutResponse) -> Bool { + if lhs._requestID != rhs._requestID {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + extension Netclode_V1_SnapshotCreatedResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = _protobuf_package + ".SnapshotCreatedResponse" public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}session_id\0\u{1}snapshot\0") diff --git a/clients/ios/Netclode/Generated/netclode/v1/common.pb.swift b/clients/ios/Netclode/Generated/netclode/v1/common.pb.swift index b6ac0669..9301255a 100644 --- a/clients/ios/Netclode/Generated/netclode/v1/common.pb.swift +++ b/clients/ios/Netclode/Generated/netclode/v1/common.pb.swift @@ -298,56 +298,56 @@ public struct Netclode_V1_Session: Sendable { public var repos: [String] = [] public var repoAccess: Netclode_V1_RepoAccess { - get {return _repoAccess ?? .unspecified} + get {_repoAccess ?? .unspecified} set {_repoAccess = newValue} } /// Returns true if `repoAccess` has been explicitly set. - public var hasRepoAccess: Bool {return self._repoAccess != nil} + public var hasRepoAccess: Bool {self._repoAccess != nil} /// Clears the value of `repoAccess`. Subsequent reads from it will return its default value. public mutating func clearRepoAccess() {self._repoAccess = nil} public var createdAt: SwiftProtobuf.Google_Protobuf_Timestamp { - get {return _createdAt ?? SwiftProtobuf.Google_Protobuf_Timestamp()} + get {_createdAt ?? SwiftProtobuf.Google_Protobuf_Timestamp()} set {_createdAt = newValue} } /// Returns true if `createdAt` has been explicitly set. - public var hasCreatedAt: Bool {return self._createdAt != nil} + public var hasCreatedAt: Bool {self._createdAt != nil} /// Clears the value of `createdAt`. Subsequent reads from it will return its default value. public mutating func clearCreatedAt() {self._createdAt = nil} public var lastActiveAt: SwiftProtobuf.Google_Protobuf_Timestamp { - get {return _lastActiveAt ?? SwiftProtobuf.Google_Protobuf_Timestamp()} + get {_lastActiveAt ?? SwiftProtobuf.Google_Protobuf_Timestamp()} set {_lastActiveAt = newValue} } /// Returns true if `lastActiveAt` has been explicitly set. - public var hasLastActiveAt: Bool {return self._lastActiveAt != nil} + public var hasLastActiveAt: Bool {self._lastActiveAt != nil} /// Clears the value of `lastActiveAt`. Subsequent reads from it will return its default value. public mutating func clearLastActiveAt() {self._lastActiveAt = nil} public var sdkType: Netclode_V1_SdkType { - get {return _sdkType ?? .unspecified} + get {_sdkType ?? .unspecified} set {_sdkType = newValue} } /// Returns true if `sdkType` has been explicitly set. - public var hasSdkType: Bool {return self._sdkType != nil} + public var hasSdkType: Bool {self._sdkType != nil} /// Clears the value of `sdkType`. Subsequent reads from it will return its default value. public mutating func clearSdkType() {self._sdkType = nil} public var model: String { - get {return _model ?? String()} + get {_model ?? String()} set {_model = newValue} } /// Returns true if `model` has been explicitly set. - public var hasModel: Bool {return self._model != nil} + public var hasModel: Bool {self._model != nil} /// Clears the value of `model`. Subsequent reads from it will return its default value. public mutating func clearModel() {self._model = nil} public var copilotBackend: Netclode_V1_CopilotBackend { - get {return _copilotBackend ?? .unspecified} + get {_copilotBackend ?? .unspecified} set {_copilotBackend = newValue} } /// Returns true if `copilotBackend` has been explicitly set. - public var hasCopilotBackend: Bool {return self._copilotBackend != nil} + public var hasCopilotBackend: Bool {self._copilotBackend != nil} /// Clears the value of `copilotBackend`. Subsequent reads from it will return its default value. public mutating func clearCopilotBackend() {self._copilotBackend = nil} @@ -370,30 +370,30 @@ public struct Netclode_V1_SessionSummary: Sendable { // methods supported on all messages. public var session: Netclode_V1_Session { - get {return _session ?? Netclode_V1_Session()} + get {_session ?? Netclode_V1_Session()} set {_session = newValue} } /// Returns true if `session` has been explicitly set. - public var hasSession: Bool {return self._session != nil} + public var hasSession: Bool {self._session != nil} /// Clears the value of `session`. Subsequent reads from it will return its default value. public mutating func clearSession() {self._session = nil} public var messageCount: Int32 { - get {return _messageCount ?? 0} + get {_messageCount ?? 0} set {_messageCount = newValue} } /// Returns true if `messageCount` has been explicitly set. - public var hasMessageCount: Bool {return self._messageCount != nil} + public var hasMessageCount: Bool {self._messageCount != nil} /// Clears the value of `messageCount`. Subsequent reads from it will return its default value. public mutating func clearMessageCount() {self._messageCount = nil} /// Cursor for resuming public var lastStreamID: String { - get {return _lastStreamID ?? String()} + get {_lastStreamID ?? String()} set {_lastStreamID = newValue} } /// Returns true if `lastStreamID` has been explicitly set. - public var hasLastStreamID: Bool {return self._lastStreamID != nil} + public var hasLastStreamID: Bool {self._lastStreamID != nil} /// Clears the value of `lastStreamID`. Subsequent reads from it will return its default value. public mutating func clearLastStreamID() {self._lastStreamID = nil} @@ -413,160 +413,151 @@ public struct Netclode_V1_SessionConfig: @unchecked Sendable { // methods supported on all messages. public var sessionID: String { - get {return _storage._sessionID} + get {_storage._sessionID} set {_uniqueStorage()._sessionID = newValue} } public var workspaceDir: String { - get {return _storage._workspaceDir} + get {_storage._workspaceDir} set {_uniqueStorage()._workspaceDir = newValue} } public var githubToken: String { - get {return _storage._githubToken ?? String()} + get {_storage._githubToken ?? String()} set {_uniqueStorage()._githubToken = newValue} } /// Returns true if `githubToken` has been explicitly set. - public var hasGithubToken: Bool {return _storage._githubToken != nil} + public var hasGithubToken: Bool {_storage._githubToken != nil} /// Clears the value of `githubToken`. Subsequent reads from it will return its default value. public mutating func clearGithubToken() {_uniqueStorage()._githubToken = nil} public var repos: [String] { - get {return _storage._repos} + get {_storage._repos} set {_uniqueStorage()._repos = newValue} } public var repoAccess: Netclode_V1_RepoAccess { - get {return _storage._repoAccess ?? .unspecified} + get {_storage._repoAccess ?? .unspecified} set {_uniqueStorage()._repoAccess = newValue} } /// Returns true if `repoAccess` has been explicitly set. - public var hasRepoAccess: Bool {return _storage._repoAccess != nil} + public var hasRepoAccess: Bool {_storage._repoAccess != nil} /// Clears the value of `repoAccess`. Subsequent reads from it will return its default value. public mutating func clearRepoAccess() {_uniqueStorage()._repoAccess = nil} public var controlPlaneURL: String { - get {return _storage._controlPlaneURL} + get {_storage._controlPlaneURL} set {_uniqueStorage()._controlPlaneURL = newValue} } public var sdkType: Netclode_V1_SdkType { - get {return _storage._sdkType ?? .unspecified} + get {_storage._sdkType ?? .unspecified} set {_uniqueStorage()._sdkType = newValue} } /// Returns true if `sdkType` has been explicitly set. - public var hasSdkType: Bool {return _storage._sdkType != nil} + public var hasSdkType: Bool {_storage._sdkType != nil} /// Clears the value of `sdkType`. Subsequent reads from it will return its default value. public mutating func clearSdkType() {_uniqueStorage()._sdkType = nil} public var model: String { - get {return _storage._model ?? String()} + get {_storage._model ?? String()} set {_uniqueStorage()._model = newValue} } /// Returns true if `model` has been explicitly set. - public var hasModel: Bool {return _storage._model != nil} + public var hasModel: Bool {_storage._model != nil} /// Clears the value of `model`. Subsequent reads from it will return its default value. public mutating func clearModel() {_uniqueStorage()._model = nil} public var copilotBackend: Netclode_V1_CopilotBackend { - get {return _storage._copilotBackend ?? .unspecified} + get {_storage._copilotBackend ?? .unspecified} set {_uniqueStorage()._copilotBackend = newValue} } /// Returns true if `copilotBackend` has been explicitly set. - public var hasCopilotBackend: Bool {return _storage._copilotBackend != nil} + public var hasCopilotBackend: Bool {_storage._copilotBackend != nil} /// Clears the value of `copilotBackend`. Subsequent reads from it will return its default value. public mutating func clearCopilotBackend() {_uniqueStorage()._copilotBackend = nil} public var githubCopilotToken: String { - get {return _storage._githubCopilotToken ?? String()} + get {_storage._githubCopilotToken ?? String()} set {_uniqueStorage()._githubCopilotToken = newValue} } /// Returns true if `githubCopilotToken` has been explicitly set. - public var hasGithubCopilotToken: Bool {return _storage._githubCopilotToken != nil} + public var hasGithubCopilotToken: Bool {_storage._githubCopilotToken != nil} /// Clears the value of `githubCopilotToken`. Subsequent reads from it will return its default value. public mutating func clearGithubCopilotToken() {_uniqueStorage()._githubCopilotToken = nil} public var codexAccessToken: String { - get {return _storage._codexAccessToken ?? String()} + get {_storage._codexAccessToken ?? String()} set {_uniqueStorage()._codexAccessToken = newValue} } /// Returns true if `codexAccessToken` has been explicitly set. - public var hasCodexAccessToken: Bool {return _storage._codexAccessToken != nil} + public var hasCodexAccessToken: Bool {_storage._codexAccessToken != nil} /// Clears the value of `codexAccessToken`. Subsequent reads from it will return its default value. public mutating func clearCodexAccessToken() {_uniqueStorage()._codexAccessToken = nil} public var codexIDToken: String { - get {return _storage._codexIDToken ?? String()} + get {_storage._codexIDToken ?? String()} set {_uniqueStorage()._codexIDToken = newValue} } /// Returns true if `codexIDToken` has been explicitly set. - public var hasCodexIDToken: Bool {return _storage._codexIDToken != nil} + public var hasCodexIDToken: Bool {_storage._codexIDToken != nil} /// Clears the value of `codexIDToken`. Subsequent reads from it will return its default value. public mutating func clearCodexIDToken() {_uniqueStorage()._codexIDToken = nil} public var openaiApiKey: String { - get {return _storage._openaiApiKey ?? String()} + get {_storage._openaiApiKey ?? String()} set {_uniqueStorage()._openaiApiKey = newValue} } /// Returns true if `openaiApiKey` has been explicitly set. - public var hasOpenaiApiKey: Bool {return _storage._openaiApiKey != nil} + public var hasOpenaiApiKey: Bool {_storage._openaiApiKey != nil} /// Clears the value of `openaiApiKey`. Subsequent reads from it will return its default value. public mutating func clearOpenaiApiKey() {_uniqueStorage()._openaiApiKey = nil} - public var codexRefreshToken: String { - get {return _storage._codexRefreshToken ?? String()} - set {_uniqueStorage()._codexRefreshToken = newValue} - } - /// Returns true if `codexRefreshToken` has been explicitly set. - public var hasCodexRefreshToken: Bool {return _storage._codexRefreshToken != nil} - /// Clears the value of `codexRefreshToken`. Subsequent reads from it will return its default value. - public mutating func clearCodexRefreshToken() {_uniqueStorage()._codexRefreshToken = nil} - public var reasoningEffort: String { - get {return _storage._reasoningEffort ?? String()} + get {_storage._reasoningEffort ?? String()} set {_uniqueStorage()._reasoningEffort = newValue} } /// Returns true if `reasoningEffort` has been explicitly set. - public var hasReasoningEffort: Bool {return _storage._reasoningEffort != nil} + public var hasReasoningEffort: Bool {_storage._reasoningEffort != nil} /// Clears the value of `reasoningEffort`. Subsequent reads from it will return its default value. public mutating func clearReasoningEffort() {_uniqueStorage()._reasoningEffort = nil} public var mistralApiKey: String { - get {return _storage._mistralApiKey ?? String()} + get {_storage._mistralApiKey ?? String()} set {_uniqueStorage()._mistralApiKey = newValue} } /// Returns true if `mistralApiKey` has been explicitly set. - public var hasMistralApiKey: Bool {return _storage._mistralApiKey != nil} + public var hasMistralApiKey: Bool {_storage._mistralApiKey != nil} /// Clears the value of `mistralApiKey`. Subsequent reads from it will return its default value. public mutating func clearMistralApiKey() {_uniqueStorage()._mistralApiKey = nil} /// URL for local Ollama inference (e.g., "http://ollama.netclode.svc.cluster.local:11434") public var ollamaURL: String { - get {return _storage._ollamaURL ?? String()} + get {_storage._ollamaURL ?? String()} set {_uniqueStorage()._ollamaURL = newValue} } /// Returns true if `ollamaURL` has been explicitly set. - public var hasOllamaURL: Bool {return _storage._ollamaURL != nil} + public var hasOllamaURL: Bool {_storage._ollamaURL != nil} /// Clears the value of `ollamaURL`. Subsequent reads from it will return its default value. public mutating func clearOllamaURL() {_uniqueStorage()._ollamaURL = nil} /// OpenCode Zen API key (for paid models, empty/"public" = free tier only) public var opencodeApiKey: String { - get {return _storage._opencodeApiKey ?? String()} + get {_storage._opencodeApiKey ?? String()} set {_uniqueStorage()._opencodeApiKey = newValue} } /// Returns true if `opencodeApiKey` has been explicitly set. - public var hasOpencodeApiKey: Bool {return _storage._opencodeApiKey != nil} + public var hasOpencodeApiKey: Bool {_storage._opencodeApiKey != nil} /// Clears the value of `opencodeApiKey`. Subsequent reads from it will return its default value. public mutating func clearOpencodeApiKey() {_uniqueStorage()._opencodeApiKey = nil} /// Z.AI API key (for GLM-4.7 models via Anthropic-compatible endpoint) public var zaiApiKey: String { - get {return _storage._zaiApiKey ?? String()} + get {_storage._zaiApiKey ?? String()} set {_uniqueStorage()._zaiApiKey = newValue} } /// Returns true if `zaiApiKey` has been explicitly set. - public var hasZaiApiKey: Bool {return _storage._zaiApiKey != nil} + public var hasZaiApiKey: Bool {_storage._zaiApiKey != nil} /// Clears the value of `zaiApiKey`. Subsequent reads from it will return its default value. public mutating func clearZaiApiKey() {_uniqueStorage()._zaiApiKey = nil} @@ -588,11 +579,11 @@ public struct Netclode_V1_StreamEntry: Sendable { public var id: String = String() public var timestamp: SwiftProtobuf.Google_Protobuf_Timestamp { - get {return _timestamp ?? SwiftProtobuf.Google_Protobuf_Timestamp()} + get {_timestamp ?? SwiftProtobuf.Google_Protobuf_Timestamp()} set {_timestamp = newValue} } /// Returns true if `timestamp` has been explicitly set. - public var hasTimestamp: Bool {return self._timestamp != nil} + public var hasTimestamp: Bool {self._timestamp != nil} /// Clears the value of `timestamp`. Subsequent reads from it will return its default value. public mutating func clearTimestamp() {self._timestamp = nil} @@ -678,11 +669,11 @@ public struct Netclode_V1_Error: Sendable { public var message: String = String() public var sessionID: String { - get {return _sessionID ?? String()} + get {_sessionID ?? String()} set {_sessionID = newValue} } /// Returns true if `sessionID` has been explicitly set. - public var hasSessionID: Bool {return self._sessionID != nil} + public var hasSessionID: Bool {self._sessionID != nil} /// Clears the value of `sessionID`. Subsequent reads from it will return its default value. public mutating func clearSessionID() {self._sessionID = nil} @@ -749,11 +740,11 @@ public struct Netclode_V1_Snapshot: Sendable { public var name: String = String() public var createdAt: SwiftProtobuf.Google_Protobuf_Timestamp { - get {return _createdAt ?? SwiftProtobuf.Google_Protobuf_Timestamp()} + get {_createdAt ?? SwiftProtobuf.Google_Protobuf_Timestamp()} set {_createdAt = newValue} } /// Returns true if `createdAt` has been explicitly set. - public var hasCreatedAt: Bool {return self._createdAt != nil} + public var hasCreatedAt: Bool {self._createdAt != nil} /// Clears the value of `createdAt`. Subsequent reads from it will return its default value. public mutating func clearCreatedAt() {self._createdAt = nil} @@ -786,11 +777,11 @@ public struct Netclode_V1_GitHubRepo: Sendable { public var `private`: Bool = false public var description_p: String { - get {return _description_p ?? String()} + get {_description_p ?? String()} set {_description_p = newValue} } /// Returns true if `description_p` has been explicitly set. - public var hasDescription_p: Bool {return self._description_p != nil} + public var hasDescription_p: Bool {self._description_p != nil} /// Clears the value of `description_p`. Subsequent reads from it will return its default value. public mutating func clearDescription_p() {self._description_p = nil} @@ -814,20 +805,20 @@ public struct Netclode_V1_GitFileChange: Sendable { public var staged: Bool = false public var linesAdded: Int32 { - get {return _linesAdded ?? 0} + get {_linesAdded ?? 0} set {_linesAdded = newValue} } /// Returns true if `linesAdded` has been explicitly set. - public var hasLinesAdded: Bool {return self._linesAdded != nil} + public var hasLinesAdded: Bool {self._linesAdded != nil} /// Clears the value of `linesAdded`. Subsequent reads from it will return its default value. public mutating func clearLinesAdded() {self._linesAdded = nil} public var linesRemoved: Int32 { - get {return _linesRemoved ?? 0} + get {_linesRemoved ?? 0} set {_linesRemoved = newValue} } /// Returns true if `linesRemoved` has been explicitly set. - public var hasLinesRemoved: Bool {return self._linesRemoved != nil} + public var hasLinesRemoved: Bool {self._linesRemoved != nil} /// Clears the value of `linesRemoved`. Subsequent reads from it will return its default value. public mutating func clearLinesRemoved() {self._linesRemoved = nil} @@ -852,51 +843,51 @@ public struct Netclode_V1_ModelInfo: Sendable { public var name: String = String() public var provider: String { - get {return _provider ?? String()} + get {_provider ?? String()} set {_provider = newValue} } /// Returns true if `provider` has been explicitly set. - public var hasProvider: Bool {return self._provider != nil} + public var hasProvider: Bool {self._provider != nil} /// Clears the value of `provider`. Subsequent reads from it will return its default value. public mutating func clearProvider() {self._provider = nil} public var billingMultiplier: Double { - get {return _billingMultiplier ?? 0} + get {_billingMultiplier ?? 0} set {_billingMultiplier = newValue} } /// Returns true if `billingMultiplier` has been explicitly set. - public var hasBillingMultiplier: Bool {return self._billingMultiplier != nil} + public var hasBillingMultiplier: Bool {self._billingMultiplier != nil} /// Clears the value of `billingMultiplier`. Subsequent reads from it will return its default value. public mutating func clearBillingMultiplier() {self._billingMultiplier = nil} public var capabilities: [String] = [] public var reasoningEffort: String { - get {return _reasoningEffort ?? String()} + get {_reasoningEffort ?? String()} set {_reasoningEffort = newValue} } /// Returns true if `reasoningEffort` has been explicitly set. - public var hasReasoningEffort: Bool {return self._reasoningEffort != nil} + public var hasReasoningEffort: Bool {self._reasoningEffort != nil} /// Clears the value of `reasoningEffort`. Subsequent reads from it will return its default value. public mutating func clearReasoningEffort() {self._reasoningEffort = nil} /// For Ollama: whether the model is downloaded locally public var downloaded: Bool { - get {return _downloaded ?? false} + get {_downloaded ?? false} set {_downloaded = newValue} } /// Returns true if `downloaded` has been explicitly set. - public var hasDownloaded: Bool {return self._downloaded != nil} + public var hasDownloaded: Bool {self._downloaded != nil} /// Clears the value of `downloaded`. Subsequent reads from it will return its default value. public mutating func clearDownloaded() {self._downloaded = nil} /// For Ollama: model size in bytes public var sizeBytes: Int64 { - get {return _sizeBytes ?? 0} + get {_sizeBytes ?? 0} set {_sizeBytes = newValue} } /// Returns true if `sizeBytes` has been explicitly set. - public var hasSizeBytes: Bool {return self._sizeBytes != nil} + public var hasSizeBytes: Bool {self._sizeBytes != nil} /// Clears the value of `sizeBytes`. Subsequent reads from it will return its default value. public mutating func clearSizeBytes() {self._sizeBytes = nil} @@ -920,20 +911,20 @@ public struct Netclode_V1_CopilotAuthStatus: Sendable { public var isAuthenticated: Bool = false public var authType: String { - get {return _authType ?? String()} + get {_authType ?? String()} set {_authType = newValue} } /// Returns true if `authType` has been explicitly set. - public var hasAuthType: Bool {return self._authType != nil} + public var hasAuthType: Bool {self._authType != nil} /// Clears the value of `authType`. Subsequent reads from it will return its default value. public mutating func clearAuthType() {self._authType = nil} public var login: String { - get {return _login ?? String()} + get {_login ?? String()} set {_login = newValue} } /// Returns true if `login` has been explicitly set. - public var hasLogin: Bool {return self._login != nil} + public var hasLogin: Bool {self._login != nil} /// Clears the value of `login`. Subsequent reads from it will return its default value. public mutating func clearLogin() {self._login = nil} @@ -958,11 +949,11 @@ public struct Netclode_V1_CopilotPremiumQuota: Sendable { public var remaining: Int32 = 0 public var resetAt: String { - get {return _resetAt ?? String()} + get {_resetAt ?? String()} set {_resetAt = newValue} } /// Returns true if `resetAt` has been explicitly set. - public var hasResetAt: Bool {return self._resetAt != nil} + public var hasResetAt: Bool {self._resetAt != nil} /// Clears the value of `resetAt`. Subsequent reads from it will return its default value. public mutating func clearResetAt() {self._resetAt = nil} @@ -1137,7 +1128,7 @@ extension Netclode_V1_SessionSummary: SwiftProtobuf.Message, SwiftProtobuf._Mess extension Netclode_V1_SessionConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = _protobuf_package + ".SessionConfig" - public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}session_id\0\u{3}workspace_dir\0\u{3}github_token\0\u{1}repos\0\u{3}repo_access\0\u{3}control_plane_url\0\u{3}sdk_type\0\u{1}model\0\u{3}copilot_backend\0\u{3}github_copilot_token\0\u{3}codex_access_token\0\u{3}codex_id_token\0\u{3}openai_api_key\0\u{3}codex_refresh_token\0\u{3}reasoning_effort\0\u{3}mistral_api_key\0\u{3}ollama_url\0\u{3}opencode_api_key\0\u{3}zai_api_key\0") + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}session_id\0\u{3}workspace_dir\0\u{3}github_token\0\u{1}repos\0\u{3}repo_access\0\u{3}control_plane_url\0\u{3}sdk_type\0\u{1}model\0\u{3}copilot_backend\0\u{3}github_copilot_token\0\u{3}codex_access_token\0\u{3}codex_id_token\0\u{3}openai_api_key\0\u{4}\u{2}reasoning_effort\0\u{3}mistral_api_key\0\u{3}ollama_url\0\u{3}opencode_api_key\0\u{3}zai_api_key\0") fileprivate class _StorageClass { var _sessionID: String = String() @@ -1153,7 +1144,6 @@ extension Netclode_V1_SessionConfig: SwiftProtobuf.Message, SwiftProtobuf._Messa var _codexAccessToken: String? = nil var _codexIDToken: String? = nil var _openaiApiKey: String? = nil - var _codexRefreshToken: String? = nil var _reasoningEffort: String? = nil var _mistralApiKey: String? = nil var _ollamaURL: String? = nil @@ -1182,7 +1172,6 @@ extension Netclode_V1_SessionConfig: SwiftProtobuf.Message, SwiftProtobuf._Messa _codexAccessToken = source._codexAccessToken _codexIDToken = source._codexIDToken _openaiApiKey = source._openaiApiKey - _codexRefreshToken = source._codexRefreshToken _reasoningEffort = source._reasoningEffort _mistralApiKey = source._mistralApiKey _ollamaURL = source._ollamaURL @@ -1219,7 +1208,6 @@ extension Netclode_V1_SessionConfig: SwiftProtobuf.Message, SwiftProtobuf._Messa case 11: try { try decoder.decodeSingularStringField(value: &_storage._codexAccessToken) }() case 12: try { try decoder.decodeSingularStringField(value: &_storage._codexIDToken) }() case 13: try { try decoder.decodeSingularStringField(value: &_storage._openaiApiKey) }() - case 14: try { try decoder.decodeSingularStringField(value: &_storage._codexRefreshToken) }() case 15: try { try decoder.decodeSingularStringField(value: &_storage._reasoningEffort) }() case 16: try { try decoder.decodeSingularStringField(value: &_storage._mistralApiKey) }() case 17: try { try decoder.decodeSingularStringField(value: &_storage._ollamaURL) }() @@ -1276,9 +1264,6 @@ extension Netclode_V1_SessionConfig: SwiftProtobuf.Message, SwiftProtobuf._Messa try { if let v = _storage._openaiApiKey { try visitor.visitSingularStringField(value: v, fieldNumber: 13) } }() - try { if let v = _storage._codexRefreshToken { - try visitor.visitSingularStringField(value: v, fieldNumber: 14) - } }() try { if let v = _storage._reasoningEffort { try visitor.visitSingularStringField(value: v, fieldNumber: 15) } }() @@ -1316,7 +1301,6 @@ extension Netclode_V1_SessionConfig: SwiftProtobuf.Message, SwiftProtobuf._Messa if _storage._codexAccessToken != rhs_storage._codexAccessToken {return false} if _storage._codexIDToken != rhs_storage._codexIDToken {return false} if _storage._openaiApiKey != rhs_storage._openaiApiKey {return false} - if _storage._codexRefreshToken != rhs_storage._codexRefreshToken {return false} if _storage._reasoningEffort != rhs_storage._reasoningEffort {return false} if _storage._mistralApiKey != rhs_storage._mistralApiKey {return false} if _storage._ollamaURL != rhs_storage._ollamaURL {return false} diff --git a/clients/ios/Netclode/Generated/netclode/v1/events.pb.swift b/clients/ios/Netclode/Generated/netclode/v1/events.pb.swift index 7197aa97..ef5dcbab 100644 --- a/clients/ios/Netclode/Generated/netclode/v1/events.pb.swift +++ b/clients/ios/Netclode/Generated/netclode/v1/events.pb.swift @@ -54,6 +54,9 @@ public enum Netclode_V1_AgentEventKind: SwiftProtobuf.Enum, Swift.CaseIterable { /// Agent reconnected after disconnect case agentReconnected // = 10 + + /// Port exposure was removed + case portUnexposed // = 11 case UNRECOGNIZED(Int) public init() { @@ -73,6 +76,7 @@ public enum Netclode_V1_AgentEventKind: SwiftProtobuf.Enum, Swift.CaseIterable { case 8: self = .repoClone case 9: self = .agentDisconnected case 10: self = .agentReconnected + case 11: self = .portUnexposed default: self = .UNRECOGNIZED(rawValue) } } @@ -90,6 +94,7 @@ public enum Netclode_V1_AgentEventKind: SwiftProtobuf.Enum, Swift.CaseIterable { case .repoClone: return 8 case .agentDisconnected: return 9 case .agentReconnected: return 10 + case .portUnexposed: return 11 case .UNRECOGNIZED(let i): return i } } @@ -107,6 +112,7 @@ public enum Netclode_V1_AgentEventKind: SwiftProtobuf.Enum, Swift.CaseIterable { .repoClone, .agentDisconnected, .agentReconnected, + .portUnexposed, ] } @@ -283,6 +289,14 @@ public struct Netclode_V1_AgentEvent: Sendable { set {payload = .repoClone(newValue)} } + public var portUnexposed: Netclode_V1_PortUnexposedPayload { + get { + if case .portUnexposed(let v)? = payload {return v} + return Netclode_V1_PortUnexposedPayload() + } + set {payload = .portUnexposed(newValue)} + } + public var unknownFields = SwiftProtobuf.UnknownStorage() /// Event-specific payload @@ -295,6 +309,7 @@ public struct Netclode_V1_AgentEvent: Sendable { case toolEnd(Netclode_V1_ToolEndPayload) case portExposed(Netclode_V1_PortExposedPayload) case repoClone(Netclode_V1_RepoClonePayload) + case portUnexposed(Netclode_V1_PortUnexposedPayload) } @@ -346,11 +361,11 @@ public struct Netclode_V1_ToolStartPayload: Sendable { /// Set when tool runs inside a Task/subagent public var parentToolUseID: String { - get {return _parentToolUseID ?? String()} + get {_parentToolUseID ?? String()} set {_parentToolUseID = newValue} } /// Returns true if `parentToolUseID` has been explicitly set. - public var hasParentToolUseID: Bool {return self._parentToolUseID != nil} + public var hasParentToolUseID: Bool {self._parentToolUseID != nil} /// Clears the value of `parentToolUseID`. Subsequent reads from it will return its default value. public mutating func clearParentToolUseID() {self._parentToolUseID = nil} @@ -370,21 +385,21 @@ public struct Netclode_V1_ToolInputPayload: Sendable { /// For partial=true: delta contains the streaming chunk /// For partial=false: input contains the full input public var delta: String { - get {return _delta ?? String()} + get {_delta ?? String()} set {_delta = newValue} } /// Returns true if `delta` has been explicitly set. - public var hasDelta: Bool {return self._delta != nil} + public var hasDelta: Bool {self._delta != nil} /// Clears the value of `delta`. Subsequent reads from it will return its default value. public mutating func clearDelta() {self._delta = nil} /// Full tool input (when partial=false) public var input: SwiftProtobuf.Google_Protobuf_Struct { - get {return _input ?? SwiftProtobuf.Google_Protobuf_Struct()} + get {_input ?? SwiftProtobuf.Google_Protobuf_Struct()} set {_input = newValue} } /// Returns true if `input` has been explicitly set. - public var hasInput: Bool {return self._input != nil} + public var hasInput: Bool {self._input != nil} /// Clears the value of `input`. Subsequent reads from it will return its default value. public mutating func clearInput() {self._input = nil} @@ -405,21 +420,21 @@ public struct Netclode_V1_ToolOutputPayload: Sendable { /// For partial=true: delta contains the streaming chunk /// For partial=false: output contains the full output public var delta: String { - get {return _delta ?? String()} + get {_delta ?? String()} set {_delta = newValue} } /// Returns true if `delta` has been explicitly set. - public var hasDelta: Bool {return self._delta != nil} + public var hasDelta: Bool {self._delta != nil} /// Clears the value of `delta`. Subsequent reads from it will return its default value. public mutating func clearDelta() {self._delta = nil} /// Full tool output (when partial=false) public var output: String { - get {return _output ?? String()} + get {_output ?? String()} set {_output = newValue} } /// Returns true if `output` has been explicitly set. - public var hasOutput: Bool {return self._output != nil} + public var hasOutput: Bool {self._output != nil} /// Clears the value of `output`. Subsequent reads from it will return its default value. public mutating func clearOutput() {self._output = nil} @@ -442,31 +457,31 @@ public struct Netclode_V1_ToolEndPayload: Sendable { /// Error message if failed public var error: String { - get {return _error ?? String()} + get {_error ?? String()} set {_error = newValue} } /// Returns true if `error` has been explicitly set. - public var hasError: Bool {return self._error != nil} + public var hasError: Bool {self._error != nil} /// Clears the value of `error`. Subsequent reads from it will return its default value. public mutating func clearError() {self._error = nil} /// Duration in milliseconds public var durationMs: Int64 { - get {return _durationMs ?? 0} + get {_durationMs ?? 0} set {_durationMs = newValue} } /// Returns true if `durationMs` has been explicitly set. - public var hasDurationMs: Bool {return self._durationMs != nil} + public var hasDurationMs: Bool {self._durationMs != nil} /// Clears the value of `durationMs`. Subsequent reads from it will return its default value. public mutating func clearDurationMs() {self._durationMs = nil} /// Tool output/result (for successful tools) public var result: String { - get {return _result ?? String()} + get {_result ?? String()} set {_result = newValue} } /// Returns true if `result` has been explicitly set. - public var hasResult: Bool {return self._result != nil} + public var hasResult: Bool {self._result != nil} /// Clears the value of `result`. Subsequent reads from it will return its default value. public mutating func clearResult() {self._result = nil} @@ -490,21 +505,21 @@ public struct Netclode_V1_PortExposedPayload: Sendable { /// Process name listening on the port public var process: String { - get {return _process ?? String()} + get {_process ?? String()} set {_process = newValue} } /// Returns true if `process` has been explicitly set. - public var hasProcess: Bool {return self._process != nil} + public var hasProcess: Bool {self._process != nil} /// Clears the value of `process`. Subsequent reads from it will return its default value. public mutating func clearProcess() {self._process = nil} /// URL to access the exposed port public var previewURL: String { - get {return _previewURL ?? String()} + get {_previewURL ?? String()} set {_previewURL = newValue} } /// Returns true if `previewURL` has been explicitly set. - public var hasPreviewURL: Bool {return self._previewURL != nil} + public var hasPreviewURL: Bool {self._previewURL != nil} /// Clears the value of `previewURL`. Subsequent reads from it will return its default value. public mutating func clearPreviewURL() {self._previewURL = nil} @@ -516,6 +531,20 @@ public struct Netclode_V1_PortExposedPayload: Sendable { fileprivate var _previewURL: String? = nil } +/// PortUnexposedPayload contains data for port removal events. +public struct Netclode_V1_PortUnexposedPayload: Sendable { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// The port number no longer exposed + public var port: Int32 = 0 + + public var unknownFields = SwiftProtobuf.UnknownStorage() + + public init() {} +} + /// RepoClonePayload contains data for repository clone progress events. public struct Netclode_V1_RepoClonePayload: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the @@ -541,7 +570,7 @@ public struct Netclode_V1_RepoClonePayload: Sendable { fileprivate let _protobuf_package = "netclode.v1" extension Netclode_V1_AgentEventKind: SwiftProtobuf._ProtoNameProviding { - public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{2}\0AGENT_EVENT_KIND_UNSPECIFIED\0\u{1}AGENT_EVENT_KIND_MESSAGE\0\u{1}AGENT_EVENT_KIND_THINKING\0\u{1}AGENT_EVENT_KIND_TOOL_START\0\u{1}AGENT_EVENT_KIND_TOOL_INPUT\0\u{1}AGENT_EVENT_KIND_TOOL_OUTPUT\0\u{1}AGENT_EVENT_KIND_TOOL_END\0\u{1}AGENT_EVENT_KIND_PORT_EXPOSED\0\u{1}AGENT_EVENT_KIND_REPO_CLONE\0\u{1}AGENT_EVENT_KIND_AGENT_DISCONNECTED\0\u{1}AGENT_EVENT_KIND_AGENT_RECONNECTED\0") + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{2}\0AGENT_EVENT_KIND_UNSPECIFIED\0\u{1}AGENT_EVENT_KIND_MESSAGE\0\u{1}AGENT_EVENT_KIND_THINKING\0\u{1}AGENT_EVENT_KIND_TOOL_START\0\u{1}AGENT_EVENT_KIND_TOOL_INPUT\0\u{1}AGENT_EVENT_KIND_TOOL_OUTPUT\0\u{1}AGENT_EVENT_KIND_TOOL_END\0\u{1}AGENT_EVENT_KIND_PORT_EXPOSED\0\u{1}AGENT_EVENT_KIND_REPO_CLONE\0\u{1}AGENT_EVENT_KIND_AGENT_DISCONNECTED\0\u{1}AGENT_EVENT_KIND_AGENT_RECONNECTED\0\u{1}AGENT_EVENT_KIND_PORT_UNEXPOSED\0") } extension Netclode_V1_MessageRole: SwiftProtobuf._ProtoNameProviding { @@ -554,7 +583,7 @@ extension Netclode_V1_RepoCloneStage: SwiftProtobuf._ProtoNameProviding { extension Netclode_V1_AgentEvent: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = _protobuf_package + ".AgentEvent" - public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}kind\0\u{3}correlation_id\0\u{1}message\0\u{1}thinking\0\u{3}tool_start\0\u{3}tool_input\0\u{3}tool_output\0\u{3}tool_end\0\u{3}port_exposed\0\u{3}repo_clone\0") + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}kind\0\u{3}correlation_id\0\u{1}message\0\u{1}thinking\0\u{3}tool_start\0\u{3}tool_input\0\u{3}tool_output\0\u{3}tool_end\0\u{3}port_exposed\0\u{3}repo_clone\0\u{3}port_unexposed\0") public mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { @@ -668,6 +697,19 @@ extension Netclode_V1_AgentEvent: SwiftProtobuf.Message, SwiftProtobuf._MessageI self.payload = .repoClone(v) } }() + case 11: try { + var v: Netclode_V1_PortUnexposedPayload? + var hadOneofValue = false + if let current = self.payload { + hadOneofValue = true + if case .portUnexposed(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.payload = .portUnexposed(v) + } + }() default: break } } @@ -717,6 +759,10 @@ extension Netclode_V1_AgentEvent: SwiftProtobuf.Message, SwiftProtobuf._MessageI guard case .repoClone(let v)? = self.payload else { preconditionFailure() } try visitor.visitSingularMessageField(value: v, fieldNumber: 10) }() + case .portUnexposed?: try { + guard case .portUnexposed(let v)? = self.payload else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 11) + }() case nil: break } try unknownFields.traverse(visitor: &visitor) @@ -1011,6 +1057,36 @@ extension Netclode_V1_PortExposedPayload: SwiftProtobuf.Message, SwiftProtobuf._ } } +extension Netclode_V1_PortUnexposedPayload: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + public static let protoMessageName: String = _protobuf_package + ".PortUnexposedPayload" + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}port\0") + + public mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularInt32Field(value: &self.port) }() + default: break + } + } + } + + public func traverse(visitor: inout V) throws { + if self.port != 0 { + try visitor.visitSingularInt32Field(value: self.port, fieldNumber: 1) + } + try unknownFields.traverse(visitor: &visitor) + } + + public static func ==(lhs: Netclode_V1_PortUnexposedPayload, rhs: Netclode_V1_PortUnexposedPayload) -> Bool { + if lhs.port != rhs.port {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + extension Netclode_V1_RepoClonePayload: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = _protobuf_package + ".RepoClonePayload" public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}repo\0\u{1}stage\0\u{1}message\0") diff --git a/clients/ios/Netclode/Models/AgentEvent.swift b/clients/ios/Netclode/Models/AgentEvent.swift index a1143ce8..71ba0892 100644 --- a/clients/ios/Netclode/Models/AgentEvent.swift +++ b/clients/ios/Netclode/Models/AgentEvent.swift @@ -16,6 +16,7 @@ enum AgentEventKind: String, Codable, Sendable { case commandEnd = "command_end" case thinking case portExposed = "port_exposed" + case portUnexposed = "port_unexposed" case repoClone = "repo_clone" case agentDisconnected = "agent_disconnected" case agentReconnected = "agent_reconnected" @@ -31,6 +32,7 @@ enum AgentEventKind: String, Codable, Sendable { case .commandEnd: "Command Finished" case .thinking: "Thinking" case .portExposed: "Port Exposed" + case .portUnexposed: "Port Removed" case .repoClone: "Repository" case .agentDisconnected: "Connection Lost" case .agentReconnected: "Reconnected" @@ -44,6 +46,7 @@ enum AgentEventKind: String, Codable, Sendable { case .commandStart, .commandEnd: "terminal.fill" case .thinking: "brain.head.profile" case .portExposed: "network" + case .portUnexposed: "network.slash" case .repoClone: "arrow.down.circle.fill" case .agentDisconnected: "wifi.slash" case .agentReconnected: "wifi" @@ -79,6 +82,7 @@ enum AgentEvent: Identifiable, Sendable { case commandEnd(CommandEndEvent) case thinking(ThinkingEvent) case portExposed(PortExposedEvent) + case portUnexposed(PortUnexposedEvent) case repoClone(RepoCloneEvent) case agentDisconnected(AgentDisconnectedEvent) case agentReconnected(AgentReconnectedEvent) @@ -94,6 +98,7 @@ enum AgentEvent: Identifiable, Sendable { case .commandEnd(let e): e.id case .thinking(let e): e.id case .portExposed(let e): e.id + case .portUnexposed(let e): e.id case .repoClone(let e): e.id case .agentDisconnected(let e): e.id case .agentReconnected(let e): e.id @@ -111,6 +116,7 @@ enum AgentEvent: Identifiable, Sendable { case .commandEnd: .commandEnd case .thinking: .thinking case .portExposed: .portExposed + case .portUnexposed: .portUnexposed case .repoClone: .repoClone case .agentDisconnected: .agentDisconnected case .agentReconnected: .agentReconnected @@ -128,6 +134,7 @@ enum AgentEvent: Identifiable, Sendable { case .commandEnd(let e): e.timestamp case .thinking(let e): e.timestamp case .portExposed(let e): e.timestamp + case .portUnexposed(let e): e.timestamp case .repoClone(let e): e.timestamp case .agentDisconnected(let e): e.timestamp case .agentReconnected(let e): e.timestamp @@ -239,6 +246,13 @@ struct PortExposedEvent: AgentEventProtocol { let previewUrl: String? } +struct PortUnexposedEvent: AgentEventProtocol { + let id: UUID + var kind: AgentEventKind { .portUnexposed } + let timestamp: Date + let port: Int +} + /// Repository clone/pull progress event. enum RepoCloneStage: String, Codable, Sendable { case starting diff --git a/clients/ios/Netclode/Models/ClientMessage.swift b/clients/ios/Netclode/Models/ClientMessage.swift index 41f40365..9d25ee40 100644 --- a/clients/ios/Netclode/Models/ClientMessage.swift +++ b/clients/ios/Netclode/Models/ClientMessage.swift @@ -27,6 +27,7 @@ enum ClientMessage: Encodable, Sendable { case terminalInput(sessionId: String, data: String) case terminalResize(sessionId: String, cols: Int, rows: Int) case portExpose(sessionId: String, port: Int) + case portUnexpose(sessionId: String, port: Int) // Sync messages case sync case sessionOpen(id: String, lastMessageId: String?, lastNotificationId: String?) @@ -109,6 +110,11 @@ enum ClientMessage: Encodable, Sendable { try container.encode(sessionId, forKey: .sessionId) try container.encode(port, forKey: .port) + case .portUnexpose(let sessionId, let port): + try container.encode("port.unexpose", forKey: .type) + try container.encode(sessionId, forKey: .sessionId) + try container.encode(port, forKey: .port) + case .sync: try container.encode("sync", forKey: .type) diff --git a/clients/ios/Netclode/Models/PersistedMessage.swift b/clients/ios/Netclode/Models/PersistedMessage.swift index b5517dd9..69e81b82 100644 --- a/clients/ios/Netclode/Models/PersistedMessage.swift +++ b/clients/ios/Netclode/Models/PersistedMessage.swift @@ -181,6 +181,13 @@ struct PersistedEvent: Codable, Sendable { previewUrl: previewUrl )) + case "port_unexposed": + return .portUnexposed(PortUnexposedEvent( + id: id, + timestamp: timestamp, + port: port ?? 0 + )) + case "repo_clone": let cloneStage: RepoCloneStage switch stage { diff --git a/clients/ios/Netclode/Models/ServerMessage.swift b/clients/ios/Netclode/Models/ServerMessage.swift index 4f0aad2b..aae7d1ee 100644 --- a/clients/ios/Netclode/Models/ServerMessage.swift +++ b/clients/ios/Netclode/Models/ServerMessage.swift @@ -68,6 +68,7 @@ enum ServerMessage: Sendable { case terminalOutput(sessionId: String, data: String) case portExposed(sessionId: String, port: Int, previewUrl: String) + case portUnexposed(sessionId: String, port: Int) case portError(sessionId: String, port: Int, error: String) case error(message: String) @@ -181,6 +182,11 @@ extension ServerMessage: Decodable { let previewUrl = try container.decode(String.self, forKey: .previewUrl) self = .portExposed(sessionId: sessionId, port: port, previewUrl: previewUrl) + case "port.unexposed": + let sessionId = try container.decode(String.self, forKey: .sessionId) + let port = try container.decode(Int.self, forKey: .port) + self = .portUnexposed(sessionId: sessionId, port: port) + case "port.error": let sessionId = try container.decode(String.self, forKey: .sessionId) let port = try container.decode(Int.self, forKey: .port) @@ -406,6 +412,13 @@ private struct RawAgentEvent: Decodable { previewUrl: previewUrl )) + case "port_unexposed": + return .portUnexposed(PortUnexposedEvent( + id: id, + timestamp: timestamp, + port: port ?? 0 + )) + case "repo_clone": let cloneStage: RepoCloneStage switch stage { diff --git a/clients/ios/Netclode/Services/ConnectService.swift b/clients/ios/Netclode/Services/ConnectService.swift index fba80d62..bbd24d1f 100644 --- a/clients/ios/Netclode/Services/ConnectService.swift +++ b/clients/ios/Netclode/Services/ConnectService.swift @@ -172,17 +172,22 @@ final class ConnectService { /// - serverURL: The base server URL (e.g., "netclode-control-plane" or "http://localhost:3000") /// - connectPort: Optional port override for the Connect protocol. If empty, uses default logic. func connect(to serverURL: String, connectPort: String = "") { - // Allow connecting from disconnected or suspended states - switch connectionState { - case .disconnected, .suspended: - break - default: - return - } - + // Always honor explicit user reconnect requests, even while reconnecting. + // This lets users recover from a stale/incorrect host without restarting the app. self.serverURL = serverURL self.connectPortOverride = connectPort - + + // Cancel any in-flight reconnect loop before starting a fresh connect attempt. + reconnectTask?.cancel() + + // Tear down active state so the next attempt uses the updated target. + receiveTask?.cancel() + keepAliveTask?.cancel() + stream = nil + client = nil + serviceClient = nil + connectionState = .disconnected(reason: .userInitiated) + Task { await performConnect() } @@ -459,6 +464,9 @@ final class ConnectService { case .portExposed(let msg): return .portExposed(sessionId: msg.sessionID, port: Int(msg.port), previewUrl: msg.previewURL) + + case .portUnexposed(let msg): + return .portUnexposed(sessionId: msg.sessionID, port: Int(msg.port)) case .githubRepos(let msg): return .githubRepos(repos: msg.repos.map { convertGitHubRepo($0) }) @@ -543,6 +551,15 @@ final class ConnectService { defaultMemoryMB: msg.defaultMemoryMb )) + case .codexAuthStarted: + return nil + + case .codexAuthStatus: + return nil + + case .codexAuthLoggedOut: + return nil + case .none: return nil } @@ -740,6 +757,14 @@ final class ConnectService { process: payload.hasProcess ? payload.process : nil, previewUrl: payload.hasPreviewURL ? payload.previewURL : nil )) + + case .portUnexposed: + let payload = proto.portUnexposed + return .portUnexposed(PortUnexposedEvent( + id: id, + timestamp: timestamp, + port: Int(payload.port) + )) case .repoClone: let payload = proto.repoClone @@ -896,7 +921,7 @@ final class ConnectService { ) } - case .thinking, .toolStart, .toolInput, .toolOutput, .toolEnd, .portExposed, .repoClone, .agentDisconnected, .agentReconnected: + case .thinking, .toolStart, .toolInput, .toolOutput, .toolEnd, .portExposed, .portUnexposed, .repoClone, .agentDisconnected, .agentReconnected: let timestamp = entry.hasTimestamp ? entry.timestamp.date : Date() let event = convertAgentEvent(agentEvent, timestamp: timestamp, partial: entry.partial) return .agentEvent(sessionId: sessionId, event: event) @@ -1006,6 +1031,11 @@ final class ConnectService { port = Int(payload.port) process = payload.hasProcess ? payload.process : nil previewUrl = payload.hasPreviewURL ? payload.previewURL : nil + + case .portUnexposed: + kind = "port_unexposed" + let payload = proto.portUnexposed + port = Int(payload.port) case .repoClone: kind = "repo_clone" @@ -1261,7 +1291,7 @@ final class ConnectService { private func recordActivity() { lastActivityAt = Date() } - + private func convertToProtoMessage(_ message: ClientMessage) -> Netclode_V1_ClientMessage { var proto = Netclode_V1_ClientMessage() @@ -1355,6 +1385,12 @@ final class ConnectService { req.sessionID = sessionId req.port = Int32(port) proto.message = .exposePort(req) + + case .portUnexpose(let sessionId, let port): + var req = Netclode_V1_UnexposePortRequest() + req.sessionID = sessionId + req.port = Int32(port) + proto.message = .unexposePort(req) case .sessionOpen(let id, let lastMessageId, let lastNotificationId): var req = Netclode_V1_OpenSessionRequest() diff --git a/clients/ios/Netclode/Services/MessageRouter.swift b/clients/ios/Netclode/Services/MessageRouter.swift index 127eae7f..8687324d 100644 --- a/clients/ios/Netclode/Services/MessageRouter.swift +++ b/clients/ios/Netclode/Services/MessageRouter.swift @@ -63,6 +63,7 @@ final class MessageRouter { case .sessionCreated(let session): print("[MessageRouter] session.created received: id=\(session.id), pendingPromptText=\(sessionStore.pendingPromptText ?? "nil")") sessionStore.addSession(session) + sessionStore.clearPendingCreationError() // If there's a pending prompt, set up navigation and mark as processing // Note: The prompt itself is sent via initialPrompt in session.create, @@ -126,6 +127,8 @@ final class MessageRouter { print("Session error \(id ?? "unknown"): \(error)") if let id { sessionStore.setError(for: id, error: error) + } else if sessionStore.pendingPromptText != nil { + sessionStore.failPendingCreation(with: error) } // Agent messages @@ -265,12 +268,18 @@ final class MessageRouter { case .portExposed(let sessionId, let port, let previewUrl): print("[MessageRouter] Port \(port) exposed for session \(sessionId): \(previewUrl)") + case .portUnexposed(let sessionId, let port): + print("[MessageRouter] Port \(port) unexposed for session \(sessionId)") + case .portError(let sessionId, let port, let error): print("[MessageRouter] Failed to expose port \(port) for session \(sessionId): \(error)") // General errors case .error(let message): print("Server error: \(message)") + if sessionStore.pendingPromptText != nil { + sessionStore.failPendingCreation(with: message) + } // Notify GitHubStore in case it's waiting for a response if githubStore.isLoading { githubStore.handleError(message) diff --git a/clients/ios/Netclode/Stores/SessionStore.swift b/clients/ios/Netclode/Stores/SessionStore.swift index cf91c4ad..704043fe 100644 --- a/clients/ios/Netclode/Stores/SessionStore.swift +++ b/clients/ios/Netclode/Stores/SessionStore.swift @@ -13,6 +13,8 @@ final class SessionStore { var pendingPromptText: String? /// Session ID to navigate to and send prompt (after session is created) var pendingSessionId: String? + /// Error shown when creating a new session fails before a session ID exists. + var pendingCreationError: String? var currentSession: Session? { guard let id = currentSessionId else { return nil } @@ -65,6 +67,7 @@ final class SessionStore { lastNotificationIds.removeAll() pendingPromptText = nil pendingSessionId = nil + pendingCreationError = nil } func setCurrentSession(id: String?) { @@ -95,6 +98,18 @@ final class SessionStore { errorsBySession[sessionId] } + // MARK: - New Session Creation State + + func clearPendingCreationError() { + pendingCreationError = nil + } + + func failPendingCreation(with error: String) { + pendingPromptText = nil + pendingSessionId = nil + pendingCreationError = error + } + // MARK: - Notification Cursor (for reconnection) func setLastNotificationId(for sessionId: String, notificationId: String) { diff --git a/clients/ios/Netclode/Stores/SettingsStore.swift b/clients/ios/Netclode/Stores/SettingsStore.swift index dfdc68fa..d8433e23 100644 --- a/clients/ios/Netclode/Stores/SettingsStore.swift +++ b/clients/ios/Netclode/Stores/SettingsStore.swift @@ -4,6 +4,8 @@ import SwiftUI @MainActor @Observable final class SettingsStore { + private let lastModelKeyPrefix = "netclode_last_model_" + var serverURL: String { didSet { UserDefaults.standard.set(serverURL, forKey: "netclode_server_url") @@ -48,4 +50,18 @@ final class SettingsStore { hapticFeedbackEnabled = UserDefaults.standard.object(forKey: "netclode_haptic_feedback") as? Bool ?? true } + + /// Returns the last model selected by the user for the given SDK type. + func lastUsedModelId(for sdkType: SdkType) -> String? { + UserDefaults.standard.string(forKey: lastModelKey(for: sdkType)) + } + + /// Persists the last model selected by the user for the given SDK type. + func setLastUsedModelId(_ modelId: String, for sdkType: SdkType) { + UserDefaults.standard.set(modelId, forKey: lastModelKey(for: sdkType)) + } + + private func lastModelKey(for sdkType: SdkType) -> String { + "\(lastModelKeyPrefix)\(sdkType.rawValue)" + } } diff --git a/clients/ios/Netclode/Stores/UnifiedModelsStore.swift b/clients/ios/Netclode/Stores/UnifiedModelsStore.swift index e7adae3f..9468e2da 100644 --- a/clients/ios/Netclode/Stores/UnifiedModelsStore.swift +++ b/clients/ios/Netclode/Stores/UnifiedModelsStore.swift @@ -147,4 +147,214 @@ final class UnifiedModelsStore { case .codex: return defaultCodexModelId } } + + /// Resolve the preferred model for an SDK: + /// 1) last used model (if still available) + /// 2) latest versioned family match from backend list + /// 3) static default (legacy fallback) + func preferredModelId(for sdkType: SdkType, lastUsedModelId: String?) -> String { + let availableModels = models(for: sdkType) + let defaultModelId = Self.defaultModelId(for: sdkType) + + if let lastUsedModelId, availableModels.contains(where: { $0.id == lastUsedModelId }) { + return lastUsedModelId + } + + switch sdkType { + case .codex: + if let latestCodex = findLatestCodexModelId(in: availableModels) { + return latestCodex + } + case .claude, .opencode, .copilot: + if let latestClaudeOpus = findLatestClaudeOpusModelId(in: availableModels) { + return latestClaudeOpus + } + } + + if availableModels.contains(where: { $0.id == defaultModelId }) { + return defaultModelId + } + + return defaultModelId + } + + private static let codexEffortOrder: [String] = ["medium", "low", "high", "xhigh", "minimal"] + private static let codexAuthOrder: [String] = ["oauth", "api"] + + private func findLatestCodexModelId(in models: [CopilotModel]) -> String? { + struct Candidate { + let model: CopilotModel + let baseId: String + let version: [Int] + let auth: String? + let effort: String? + } + + let candidates: [Candidate] = models.compactMap { model in + let (baseId, auth, effort) = parseCodexModelParts(id: model.id) + guard let version = extractVersion(in: baseId, family: "gpt", suffix: "codex") else { + return nil + } + return Candidate(model: model, baseId: baseId, version: version, auth: auth, effort: effort) + } + + guard !candidates.isEmpty else { return nil } + + guard let bestVersion = candidates.map(\.version).max(by: isVersionLess) else { + return nil + } + + let sameVersion = candidates.filter { !isVersionLess($0.version, bestVersion) && !isVersionLess(bestVersion, $0.version) } + let sameBase = sameVersion.filter { $0.baseId == sameVersion.map(\.baseId).sorted().first } + let target = sameBase.isEmpty ? sameVersion : sameBase + + let sorted = target.sorted { lhs, rhs in + let lhsEffortRank = Self.codexEffortOrder.firstIndex(of: lhs.effort ?? "") ?? Int.max + let rhsEffortRank = Self.codexEffortOrder.firstIndex(of: rhs.effort ?? "") ?? Int.max + if lhsEffortRank != rhsEffortRank { return lhsEffortRank < rhsEffortRank } + + let lhsAuthRank = Self.codexAuthOrder.firstIndex(of: lhs.auth ?? "") ?? Int.max + let rhsAuthRank = Self.codexAuthOrder.firstIndex(of: rhs.auth ?? "") ?? Int.max + if lhsAuthRank != rhsAuthRank { return lhsAuthRank < rhsAuthRank } + + return lhs.model.id < rhs.model.id + } + + return sorted.first?.model.id + } + + private func findLatestClaudeOpusModelId(in models: [CopilotModel]) -> String? { + struct Candidate { + let model: CopilotModel + let version: [Int] + let hasSuffix: Bool + } + + let candidates: [Candidate] = models.compactMap { model in + guard let version = extractClaudeOpusVersion(for: model) else { + return nil + } + return Candidate( + model: model, + version: version, + hasSuffix: model.id.contains(":") + ) + } + + guard !candidates.isEmpty else { return nil } + + let sorted = candidates.sorted { lhs, rhs in + if !isVersionEqual(lhs.version, rhs.version) { + return isVersionLess(rhs.version, lhs.version) + } + if lhs.hasSuffix != rhs.hasSuffix { + return !lhs.hasSuffix + } + return lhs.model.id < rhs.model.id + } + return sorted.first?.model.id + } + + private func extractClaudeOpusVersion(for model: CopilotModel) -> [Int]? { + let nameVersion = extractVersion( + in: normalizeModelToken(model.name), + family: "claude-opus", + suffix: nil, + maxComponents: 2 + ) + let idVersion = extractVersion( + in: normalizeModelToken(model.id), + family: "claude-opus", + suffix: nil, + maxComponents: 2 + ) + + // Prefer human-facing name parse to avoid date-coded IDs dominating ranking. + let raw = nameVersion ?? idVersion + guard var version = raw else { return nil } + + // Drop date-like second component (e.g. 20250514 from claude-opus-4-20250514). + if version.count >= 2 && version[1] >= 1000 { + version = [version[0]] + } + return version + } + + private func parseCodexModelParts(id: String) -> (baseId: String, auth: String?, effort: String?) { + let parts = id.split(separator: ":").map(String.init) + guard !parts.isEmpty else { return (id, nil, nil) } + let baseId = parts[0] + + if parts.count >= 3 { + return (baseId, parts[1].lowercased(), parts[2].lowercased()) + } + if parts.count == 2 { + let maybe = parts[1].lowercased() + if Self.codexAuthOrder.contains(maybe) { + return (baseId, maybe, nil) + } + if Self.codexEffortOrder.contains(maybe) { + return (baseId, nil, maybe) + } + } + return (baseId, nil, nil) + } + + private func extractVersion(in rawText: String, family: String, suffix: String?, maxComponents: Int? = nil) -> [Int]? { + let normalized = normalizeModelToken(rawText) + let escapedFamily = NSRegularExpression.escapedPattern(for: family) + let escapedSuffix = suffix.map(NSRegularExpression.escapedPattern(for:)) ?? "" + let separatorClause: String + if let maxComponents { + let extra = max(0, maxComponents - 1) + separatorClause = "(?:[.-][0-9]+){0,\(extra)}" + } else { + separatorClause = "(?:[.-][0-9]+)*" + } + let pattern: String + + if suffix == nil { + pattern = "\(escapedFamily)-([0-9]+\(separatorClause))" + } else { + pattern = "\(escapedFamily)-([0-9]+\(separatorClause))-\(escapedSuffix)" + } + + guard let regex = try? NSRegularExpression(pattern: pattern, options: [.caseInsensitive]) else { + return nil + } + let nsText = normalized as NSString + guard let match = regex.firstMatch(in: normalized, options: [], range: NSRange(location: 0, length: nsText.length)), + match.numberOfRanges >= 2 else { + return nil + } + + let versionString = nsText.substring(with: match.range(at: 1)) + let parts = versionString + .split(whereSeparator: { $0 == "." || $0 == "-" }) + .compactMap { Int($0) } + return parts.isEmpty ? nil : parts + } + + private func normalizeModelToken(_ token: String) -> String { + token.lowercased() + .replacingOccurrences(of: "_", with: "-") + .replacingOccurrences(of: " ", with: "-") + .replacingOccurrences(of: "/", with: "-") + } + + private func isVersionLess(_ lhs: [Int], _ rhs: [Int]) -> Bool { + let count = max(lhs.count, rhs.count) + for idx in 0.. Bool { + !isVersionLess(lhs, rhs) && !isVersionLess(rhs, lhs) + } } diff --git a/clients/ios/README.md b/clients/ios/README.md index 0772f999..e99098f3 100644 --- a/clients/ios/README.md +++ b/clients/ios/README.md @@ -38,6 +38,34 @@ make run-ios SIMULATOR="iPhone 16" make run-device ``` +### Signing setup (required for CLI builds) + +`xcodebuild` needs an Apple Developer account and a valid development signing certificate in your keychain. + +1. Open Xcode → Settings → Accounts, add your Apple ID, and select a team. +2. In that team, click **Manage Certificates...** and create/download a development certificate. +3. Verify certificates are visible to the CLI: + +```bash +security find-identity -v -p codesigning +``` + +If you are not using the default project team, pass your team explicitly: + +```bash +make run-macos TEAM_ID= +make run-ios TEAM_ID= +make run-device TEAM_ID= +``` + +`make` now auto-detects `TEAM_ID` from your local Apple Development certificate (or falls back to your first local provisioning profile) if you do not pass `TEAM_ID`. + +Inspect the detected value: + +```bash +make print-ios-team-id +``` + ## Testing Run unit tests from Xcode (`⌘U`) or via command line: diff --git a/docs/deployment.md b/docs/deployment.md index 89b65f1e..4551e2eb 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -5,9 +5,13 @@ Here's how to get Netclode running on your own server. ## Prerequisites - Linux machine with nested virtualization (2 vCPU, 8GB RAM minimum) -- S3-compatible storage (DigitalOcean Spaces, Cloudflare R2, etc.) +- S3-compatible storage backend: + - External provider (DigitalOcean Spaces, Cloudflare R2, etc.), or + - Self-hosted MinIO on the same server (optional, can be auto-configured) - Tailscale account -- At least one LLM API key (Anthropic, OpenAI, Mistral, etc.) - see [SDK Support](sdk-support.md) +- LLM credentials: + - At least one API key (Anthropic, OpenAI, Mistral, etc.) for non-Codex providers, or + - Local CLI Codex OAuth login (`netclode auth codex`) for Codex `:oauth` sessions without API keys - Ansible installed locally ## 1. Clone the repo @@ -37,27 +41,47 @@ Your server is now accessible via its Tailscale hostname (e.g., `my-server`). ## 4. Configure Tailscale for k8s ingress -1. Create an [OAuth client](https://login.tailscale.com/admin/settings/oauth) with **Devices: Write** scope -2. Enable [MagicDNS](https://login.tailscale.com/admin/dns) +1. Create an [OAuth client](https://login.tailscale.com/admin/settings/oauth) with these scopes: + - `General` -> `Services` + - `Devices` -> `Core` + - `Keys` -> `Auth Keys` +2. Allow these tags for the OAuth client: + - `tag:k8s-operator` + - `tag:k8s` +3. Ensure both tags exist in your tailnet policy and `tag:k8s-operator` can own `tag:k8s`. +4. Enable [MagicDNS](https://login.tailscale.com/admin/dns). +5. Enable tailnet HTTPS certificates (DNS page -> HTTPS certificates). Without this, the ingress proxy cannot serve on `:443`. ## 5. Configure secrets Create `.env` at the repo root: ```bash -# LLM provider (at least one required - see docs/sdk-support.md) -ANTHROPIC_API_KEY=sk-ant-api03-xxx +# LLM credentials (choose one path - see docs/sdk-support.md) +# Path A: API key(s) +# ANTHROPIC_API_KEY=sk-ant-api03-xxx # OPENAI_API_KEY=sk-xxx # MISTRAL_API_KEY=xxx +# +# Optional: required only if using Codex OAuth sessions +# 32-byte base64 key for encrypting session OAuth refresh tokens at rest +# CODEX_OAUTH_ENCRYPTION_KEY_B64=$(openssl rand -base64 32) # Tailscale (OAuth client from step 4) TS_OAUTH_CLIENT_ID=your-oauth-client-id TS_OAUTH_CLIENT_SECRET=your-oauth-client-secret -# JuiceFS / S3 storage +# Option A: external S3-compatible storage (manual) DO_SPACES_ACCESS_KEY=your-spaces-access-key DO_SPACES_SECRET_KEY=your-spaces-secret-key JUICEFS_BUCKET=https://fra1.digitaloceanspaces.com/your-bucket + +# Option B: self-host MinIO on the same VPS (automatic) +# MINIO_ENABLED=true +# MINIO_BUCKET_NAME=netclode-juicefs +# MINIO_API_PORT=9000 + +# JuiceFS metadata (optional - default shown) JUICEFS_META_URL=redis://redis-juicefs.netclode.svc.cluster.local:6379/0 # Deployment target (Tailscale hostname from step 3) @@ -69,7 +93,9 @@ GITHUB_APP_PRIVATE_KEY_B64=base64-encoded-pem-private-key GITHUB_INSTALLATION_ID=12345678 ``` -Create a bucket (e.g., `netclode-juicefs`) with read/write credentials. +Storage notes: +- If using external S3, create a bucket (e.g., `netclode-juicefs`) with read/write credentials. +- If `MINIO_ENABLED=true`, MinIO is installed by Ansible and `deploy-secrets` auto-wires JuiceFS credentials/bucket from `/var/secrets/minio-root-*` if `DO_SPACES_*` / `JUICEFS_BUCKET` are omitted. ## 6. Install Ansible dependencies @@ -80,11 +106,20 @@ ansible-galaxy collection install -r requirements.yaml ## 7. Deploy +If your server disables root SSH login, pass the SSH user explicitly: + +```bash +ANSIBLE_USER=ubuntu +``` + ```bash cd infra/ansible # Full infrastructure deployment (reads secrets from .env) DEPLOY_HOST= ansible-playbook playbooks/site.yaml + +# Full infrastructure deployment (non-root SSH user) +DEPLOY_HOST= ansible-playbook playbooks/site.yaml -e ansible_user=$ANSIBLE_USER ``` This installs: @@ -101,6 +136,9 @@ This installs: ```bash cd infra/ansible DEPLOY_HOST= ansible-playbook playbooks/fetch-kubeconfig.yaml + +# If using non-root SSH user +DEPLOY_HOST= ansible-playbook playbooks/fetch-kubeconfig.yaml -e ansible_user=$ANSIBLE_USER ``` This merges the `netclode` context into `~/.kube/config`. Use it with: @@ -133,6 +171,12 @@ make run-macos Then go to Settings → enter `` → Connect. +If you plan to use Codex OAuth models from CLI, authenticate once locally: + +```bash +netclode auth codex +``` + For iOS, see [clients/ios/README.md](/clients/ios/README.md). ## Configuration @@ -163,6 +207,9 @@ Re-run Ansible to update infrastructure: ```bash cd infra/ansible DEPLOY_HOST= ansible-playbook playbooks/site.yaml + +# If using non-root SSH user +DEPLOY_HOST= ansible-playbook playbooks/site.yaml -e ansible_user=$ANSIBLE_USER ``` Or deploy only k8s manifests (faster): @@ -170,6 +217,33 @@ Or deploy only k8s manifests (faster): ```bash cd infra/ansible DEPLOY_HOST= ansible-playbook playbooks/k8s-only.yaml + +# If using non-root SSH user +DEPLOY_HOST= ansible-playbook playbooks/k8s-only.yaml -e ansible_user=$ANSIBLE_USER +``` + +To deploy custom images (for example, locally built images in your own GHCR namespace): + +```bash +cd infra/ansible +DEPLOY_HOST= ansible-playbook playbooks/k8s-only.yaml \ + -e ansible_user=$ANSIBLE_USER \ + -e control_plane_image=ghcr.io//netclode-control-plane: \ + -e agent_image=ghcr.io//netclode-agent: +``` + +If those images are private, also pass registry pull credentials: + +```bash +cd infra/ansible +DEPLOY_HOST= ansible-playbook playbooks/k8s-only.yaml \ + -e ansible_user=$ANSIBLE_USER \ + -e control_plane_image=ghcr.io//netclode-control-plane: \ + -e agent_image=ghcr.io//netclode-agent: \ + -e image_pull_secret_name=ghcr-pull-secret \ + -e image_pull_secret_registry=ghcr.io \ + -e image_pull_secret_username= \ + -e image_pull_secret_password= ``` To restart deployments after image updates: @@ -179,6 +253,37 @@ make rollout-control-plane make rollout-agent ``` +### Fast dev loop (no GHCR push, no Ansible workload run) + +For backend/agent iteration, you can build images directly on the dev host and patch only runtime workloads: + +```bash +# Build on DEPLOY_HOST, import into k3s containerd, patch workloads, verify +make dev-loop-remote +``` + +This path is intended for rapid local iteration only. +For canonical deployment, keep using traceable GHCR tags + Ansible (`site.yaml --tags k8s-manifests`). + +See [Fast Developer Iteration Loop](dev-iteration.md) for details and variants. + +### Fast dev loop (Ansible playbook) + +Preferred orchestration for the same fast path: + +```bash +cd /Volumes/Projects/SoftwareReferences/netclode +make dev-install-builder ANSIBLE_USER=ubuntu # one-time host setup +make dev-loop-ansible ANSIBLE_USER=ubuntu +``` + +This uses `infra/ansible/playbooks/dev-loop.yaml` with tag-scoped phases: +- `dev-build` +- `dev-deploy` +- `dev-verify` + +For when to use full deployment instead of dev deploy, see [Fast Developer Iteration Loop](dev-iteration.md#when-to-run-full-deployment-instead). + ## Rollback ```bash diff --git a/docs/dev-iteration.md b/docs/dev-iteration.md new file mode 100644 index 00000000..cd51758d --- /dev/null +++ b/docs/dev-iteration.md @@ -0,0 +1,139 @@ +# Fast Developer Iteration Loop + +This workflow is optimized for day-to-day backend/agent development. + +It avoids the slow path of: +- local build on laptop +- GHCR push/pull +- full Ansible workload redeploy + +It keeps production deployment unchanged (Ansible + versioned GHCR images). + +## What it does + +Fast dev loop (`make dev-loop-ansible` or `make dev-loop-remote`): +1. Syncs only Docker-needed sources (`services/control-plane`, `services/agent`) to the dev host +2. Builds `linux/amd64` images directly on that host +3. Imports those images into k3s containerd (`k3s ctr images import`) +4. Patches only runtime workload objects with `kubectl` +5. Verifies rollout, image wiring, and recent logs + +No registry push is required for this loop. + +## Prerequisites + +- `.env` contains `DEPLOY_HOST` (or export it in your shell) +- SSH access to `$DEPLOY_HOST` (host alias can define user in `~/.ssh/config`) +- Remote host has Docker Buildx and `k3s` +- Local machine has `rsync`, `ssh`, and `kubectl` with context `netclode` + +Optional: +- `DEPLOY_SSH_TARGET` to override SSH target independently from `DEPLOY_HOST` +- Ansible path: `sync_ssh_target` extra var to override rsync SSH target explicitly + +Install Docker builder tooling once (recommended): + +```bash +cd /Volumes/Projects/SoftwareReferences/netclode +make dev-install-builder ANSIBLE_USER=ubuntu +``` + +## Preferred command (Ansible) + +```bash +cd /Volumes/Projects/SoftwareReferences/netclode +make dev-loop-ansible ANSIBLE_USER=ubuntu +``` + +Equivalent direct playbook run: + +```bash +cd /Volumes/Projects/SoftwareReferences/netclode/infra/ansible +set -a && source ../../.env && set +a + +ansible-playbook playbooks/dev-loop.yaml \ + -e ansible_user=ubuntu +``` + +Useful tag-scoped runs: + +```bash +# Build/import only +ansible-playbook playbooks/dev-loop.yaml --tags dev-build -e ansible_user=ubuntu + +# Deploy only (use existing local images in k3s) +ansible-playbook playbooks/dev-loop.yaml --tags dev-deploy -e ansible_user=ubuntu \ + -e control_plane_image=netclode-control-plane:dev-123 \ + -e agent_image=netclode-agent:dev-123 + +# Verify only +ansible-playbook playbooks/dev-loop.yaml --tags dev-verify -e ansible_user=ubuntu +``` + +## Legacy script path + +You can still run the script-driven path: + +```bash +cd /Volumes/Projects/SoftwareReferences/netclode +make dev-loop-remote +``` + +This generates a `TAG=dev-YYYYMMDD-HHMMSS` and uses: +- `netclode-control-plane:$TAG` +- `netclode-agent:$TAG` + +## Useful variants + +Build only control-plane: + +```bash +make dev-build-remote-control-plane TAG=dev-mytag +make dev-deploy-images TAG=dev-mytag +make dev-verify +``` + +Build only agent: + +```bash +make dev-build-remote-agent TAG=dev-mytag +make dev-deploy-images TAG=dev-mytag +make dev-verify +``` + +Use a fixed custom tag: + +```bash +make dev-loop-remote TAG=dev-jane-001 +``` + +## What gets patched + +The fast deploy phase updates: +- `Deployment/control-plane` container image +- `Deployment/control-plane` env `AGENT_IMAGE` +- `SandboxTemplate/netclode-agent` container image +- `SandboxTemplate/netclode-agent` `imagePullPolicy=IfNotPresent` + +Then it refreshes the warm pool (`SandboxWarmPool/netclode-agent-pool`) so new warm sandboxes pick up the new agent image. + +## Back to canonical deployment + +This fast path is intended for developer iteration only. + +## When to run full deployment instead + +Use full deployment (`infra/ansible/playbooks/site.yaml`) when you need full state convergence, not just fast app-image iteration: + +1. Initial deployment of a new host/cluster. +2. Host-level changes (k3s, CNI, Kata, firewall, Tailscale, GPU/Ollama, MinIO, base packages). +3. Kubernetes foundation changes (CRDs, RBAC, controllers, runtime/storage classes, namespace policies). +4. Secrets/cert changes (host secrets, k8s secrets, pull secrets, secret-proxy CA). +5. Drift correction when the host or cluster may have diverged from Ansible-managed state. +6. Release/canonical rollouts where you want the same path used for production. + +Before sharing/releasing changes, use the canonical runbook: +- build and push traceable GHCR tags +- deploy manifests via Ansible (`site.yaml --tags k8s-manifests`) + +That keeps infra state and release artifacts aligned with production procedures. diff --git a/docs/sdk-support.md b/docs/sdk-support.md index 8a40135a..702ad3b1 100644 --- a/docs/sdk-support.md +++ b/docs/sdk-support.md @@ -91,7 +91,17 @@ Use your ChatGPT subscription instead of API credits: netclode auth codex ``` -Opens browser flow and outputs tokens for your `.env`. +Opens browser flow and stores tokens locally for CLI Codex `:oauth` sessions. + +Create OAuth-backed sessions with a Codex `:oauth` model, for example: + +```bash +netclode sessions create --repo owner/repo --sdk codex --model gpt-5-codex:oauth:high +``` + +Notes: +- iOS OAuth login flow is not implemented yet (CLI-first rollout). +- Control-plane stores refresh tokens encrypted and only sends short-lived access/id tokens to agent sandboxes. ### Models @@ -110,7 +120,7 @@ Specify SDK and model when creating a session: netclode sessions create --repo owner/repo --sdk claude netclode sessions create --repo owner/repo --sdk opencode --model anthropic/claude-sonnet-4-5-20250514 netclode sessions create --repo owner/repo --sdk copilot -netclode sessions create --repo owner/repo --sdk codex --model codex-mini-latest +netclode sessions create --repo owner/repo --sdk codex --model gpt-5-codex:oauth:high ``` Or use the iOS app model picker. @@ -124,9 +134,7 @@ Or use the iOS app model picker. | `MISTRAL_API_KEY` | OpenCode | Mistral API key | | `ZAI_API_KEY` | OpenCode | Z.AI API key (for GLM-4.7 models) | | `GITHUB_COPILOT_TOKEN` | Copilot | GitHub PAT with copilot scope | -| `CODEX_ACCESS_TOKEN` | Codex | ChatGPT OAuth access token | -| `CODEX_ID_TOKEN` | Codex | ChatGPT OAuth ID token | -| `CODEX_REFRESH_TOKEN` | Codex | ChatGPT OAuth refresh token | +| `CODEX_OAUTH_ENCRYPTION_KEY_B64` | Control plane | Base64-encoded 32-byte key for encrypting stored Codex OAuth refresh tokens | ## Local Models with Ollama diff --git a/docs/secret-proxy.md b/docs/secret-proxy.md index 9acefd89..1d5e6ed2 100644 --- a/docs/secret-proxy.md +++ b/docs/secret-proxy.md @@ -218,7 +218,8 @@ Validation flow: | `SDK_TYPE_COPILOT` | `api.github.com` | `github_copilot` | | | `copilot-proxy.githubusercontent.com` | `github_copilot` | | | `api.anthropic.com` | `anthropic` | -| `SDK_TYPE_CODEX` | `api.openai.com` | `codex_access` | + +Codex requests are passed through without placeholder replacement. The agent receives backend-managed API/OAuth credentials directly for Codex. ## Security Analysis diff --git a/go.work.sum b/go.work.sum index 4dc43dfc..5b448bac 100644 --- a/go.work.sum +++ b/go.work.sum @@ -1,12 +1,19 @@ cel.dev/expr v0.24.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw= cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10= github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0/go.mod h1:P4WPRUkOhJC13W//jWpyfJNDAIpvRbAUIYLX/4jtlE0= +github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= +github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= +github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA= +github.com/bits-and-blooms/bitset v1.22.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= +github.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao= +github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/cncf/xds/go v0.0.0-20251022180443-0feb69152e9f/go.mod h1:HlzOvOjVBOfTGSRXRyY0OiCS/3J1akRGQQpRO/7zyF4= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/envoyproxy/go-control-plane v0.13.5-0.20251024222203-75eaa193e329/go.mod h1:Alz8LEClvR7xKsrq3qzoc4N0guvVNSS8KmSChGYr9hs= github.com/envoyproxy/go-control-plane/envoy v1.35.0/go.mod h1:09qwbGVuSWWAyN5t/b3iyVfz5+z8QWGrzkoqm/8SbEs= github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4= @@ -17,12 +24,14 @@ github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl76 github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo= github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA= github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/moby/spdystream v0.5.0 h1:7r0J1Si3QO/kjRitvSLVVFUjxMEb/YLj6S9FF62JBCU= github.com/moby/spdystream v0.5.0/go.mod h1:xBAYlnt/ay+11ShkdFKNAG7LsyK/tmNBVvVOwrfMgdI= github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus= github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= +github.com/sahilm/fuzzy v0.1.1/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y= github.com/spiffe/go-spiffe/v2 v2.6.0/go.mod h1:gm2SeUoMZEtpnzPNs2Csc0D/gX33k1xIx7lEzqblHEs= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.opentelemetry.io/contrib/detectors/gcp v1.38.0/go.mod h1:SU+iU7nu5ud4oCb3LQOhIZ3nRLj6FNVrKgtflbaf2ts= diff --git a/infra/ansible/README.md b/infra/ansible/README.md index a2e001cc..59b9cf0b 100644 --- a/infra/ansible/README.md +++ b/infra/ansible/README.md @@ -52,24 +52,37 @@ export DEPLOY_HOST=your-server-ip ### Secrets -All secrets are read from the `.env` file at the repo root. Required entries: +All secrets are read from the `.env` file at the repo root. + +Required entries: ```bash # .env file -# LLM provider (at least one required) -ANTHROPIC_API_KEY=sk-ant-api03-xxx +# LLM credentials (choose one path) +# Path A: API key(s) +# ANTHROPIC_API_KEY=sk-ant-api03-xxx # OPENAI_API_KEY=sk-xxx # MISTRAL_API_KEY=xxx +# +# Optional: required only for Codex OAuth session storage encryption +# CODEX_OAUTH_ENCRYPTION_KEY_B64=$(openssl rand -base64 32) # Tailscale (OAuth client for k8s ingress) TS_OAUTH_CLIENT_ID=your-oauth-client-id TS_OAUTH_CLIENT_SECRET=your-oauth-client-secret -# JuiceFS / S3 storage +# Option A: external S3-compatible storage (manual) DO_SPACES_ACCESS_KEY=your-spaces-access-key DO_SPACES_SECRET_KEY=your-spaces-secret-key JUICEFS_BUCKET=https://fra1.digitaloceanspaces.com/your-bucket + +# Option B: self-host MinIO (automatic) +# MINIO_ENABLED=true +# MINIO_BUCKET_NAME=netclode-juicefs +# MINIO_API_PORT=9000 + +# JuiceFS metadata (optional - default shown) JUICEFS_META_URL=redis://redis-juicefs.netclode.svc.cluster.local:6379/0 # GitHub App (optional - for repo picker) @@ -98,6 +111,28 @@ This creates: - `netclode-secrets` - LLM API keys and optional GitHub App credentials - `juicefs-secret` - S3 credentials and JuiceFS metadata URL +### MinIO (Optional) + +To run MinIO on the host as self-hosted S3 storage: + +```bash +MINIO_ENABLED=true ansible-playbook playbooks/site.yaml --tags "nftables,minio" +``` + +Credential resolution order: +- `minio_root_user` / `minio_root_password` Ansible vars (if provided) +- `.env` fallback: `DO_SPACES_ACCESS_KEY` / `DO_SPACES_SECRET_KEY` +- If neither is set, random credentials are generated and persisted to `/var/secrets/minio-root-user` and `/var/secrets/minio-root-password` + +When `MINIO_ENABLED=true`, `deploy-secrets` can auto-wire JuiceFS storage: +- Reads credentials from `/var/secrets/minio-root-*` if `DO_SPACES_*` are omitted +- Derives `JUICEFS_BUCKET` from host IP + MinIO port/bucket if `JUICEFS_BUCKET` is omitted + +Optional environment overrides: +- `MINIO_BUCKET_NAME` (default: `netclode-juicefs`) +- `MINIO_API_PORT` (default: `9000`) +- `MINIO_CONSOLE_PORT` (default: `9001`) + ## Usage ### Full Deployment @@ -130,6 +165,41 @@ ansible-playbook playbooks/site.yaml --skip-tags k8s-manifests # Deploy only k8s manifests (fast updates) ansible-playbook playbooks/k8s-only.yaml + +# Deploy only k8s manifests with custom images +ansible-playbook playbooks/k8s-only.yaml \ + -e control_plane_image=ghcr.io//netclode-control-plane: \ + -e agent_image=ghcr.io//netclode-agent: + +# Deploy private custom images (adds/uses imagePullSecret) +ansible-playbook playbooks/k8s-only.yaml \ + -e control_plane_image=ghcr.io//netclode-control-plane: \ + -e agent_image=ghcr.io//netclode-agent: \ + -e image_pull_secret_name=ghcr-pull-secret \ + -e image_pull_secret_registry=ghcr.io \ + -e image_pull_secret_username= \ + -e image_pull_secret_password= + +# Fast dev loop (build on target host + patch workloads + verify) +# No GHCR push/pull, no full k8s-manifests convergence. +ansible-playbook playbooks/dev-loop.yaml -e ansible_user=ubuntu + +# Install Docker + Buildx on target host for dev builds (one-time setup) +ansible-playbook playbooks/dev-builder.yaml -e ansible_user=ubuntu + +# Fast dev loop phases +ansible-playbook playbooks/dev-loop.yaml --tags dev-build -e ansible_user=ubuntu +ansible-playbook playbooks/dev-loop.yaml --tags dev-deploy -e ansible_user=ubuntu \ + -e control_plane_image=netclode-control-plane:dev-123 \ + -e agent_image=netclode-agent:dev-123 +ansible-playbook playbooks/dev-loop.yaml --tags dev-verify -e ansible_user=ubuntu + +# Optional: override rsync SSH target used by dev-build sync phase +ansible-playbook playbooks/dev-loop.yaml --tags dev-build -e ansible_user=ubuntu \ + -e sync_ssh_target=ubuntu@your-server + +# Install/update MinIO only +MINIO_ENABLED=true ansible-playbook playbooks/site.yaml --tags "nftables,minio" ``` ### Local kubectl Access @@ -160,6 +230,8 @@ kubectl config use-context netclode | `common` | Base packages, SSH, directories | | `nftables` | Firewall configuration | | `secrets` | Deploy secrets (host + k8s) | +| `minio` | MinIO self-hosted S3 storage | +| `storage` | Storage stack helpers (currently MinIO) | | `tailscale` | Tailscale daemon | | `kata` | Kata Containers runtime (use with `secrets` tag to read .env) | | `k3s` | k3s Kubernetes server | @@ -182,6 +254,7 @@ kubectl config use-context netclode | `common` | Base system setup (packages, SSH, kernel modules) | | `nftables` | Firewall with persistence | | `deploy-secrets` | Deploy secrets from .env to host and k8s | +| `minio` | Install MinIO service and bootstrap JuiceFS bucket | | `tailscale` | Tailscale daemon + auto-connect | | `kata` | Kata Containers static release | | `nvidia` | NVIDIA driver, container toolkit, device plugin (optional) | @@ -191,6 +264,7 @@ kubectl config use-context netclode | `tailscale-operator` | Tailscale K8s Operator via Helm | | `k8s-manifests` | Deploy all k8s manifests from infra/k8s/ | | `secret-proxy` | Generate CA for secret-proxy MITM sidecar | +| `dev-builder` | Install Docker + Buildx tooling for remote dev builds | ## GPU Support (Optional) @@ -384,7 +458,6 @@ SDK → auth-proxy (localhost:8080) → secret-proxy (external) → internet | `OPENCODE_API_KEY` | `api.opencode.ai`, `openrouter.ai`, `api.openrouter.ai` | | `ZAI_API_KEY` | `open.bigmodel.cn` | | `GITHUB_COPILOT_TOKEN` | `api.github.com`, `copilot-proxy.githubusercontent.com` | -| `CODEX_ACCESS_TOKEN` | `api.openai.com` | **Not proxied:** `GITHUB_TOKEN` (used by git credential helper, not HTTP headers) diff --git a/infra/ansible/group_vars/all.yaml b/infra/ansible/group_vars/all.yaml index 9706b08c..bb8ee3f1 100644 --- a/infra/ansible/group_vars/all.yaml +++ b/infra/ansible/group_vars/all.yaml @@ -85,6 +85,13 @@ tailscale_port: 41641 # Set NVIDIA_ENABLED=true in .env to install NVIDIA drivers and container toolkit nvidia_enabled: "{{ lookup('env', 'NVIDIA_ENABLED') | default('false', true) | lower == 'true' }}" +# MinIO object storage (optional) +# Set MINIO_ENABLED=true to install MinIO on the host. +minio_enabled: "{{ lookup('env', 'MINIO_ENABLED') | default('false', true) | lower == 'true' }}" +minio_bucket_name: "{{ lookup('env', 'MINIO_BUCKET_NAME') | default('netclode-juicefs', true) }}" +minio_api_port: "{{ lookup('env', 'MINIO_API_PORT') | default('9000', true) }}" +minio_console_port: "{{ lookup('env', 'MINIO_CONSOLE_PORT') | default('9001', true) }}" + # Ollama local inference (optional) # Set OLLAMA_ENABLED=true in .env to deploy Ollama with GPU support ollama_enabled: "{{ lookup('env', 'OLLAMA_ENABLED') | default('false', true) | lower == 'true' }}" diff --git a/infra/ansible/playbooks/dev-builder.yaml b/infra/ansible/playbooks/dev-builder.yaml new file mode 100644 index 00000000..2a29a549 --- /dev/null +++ b/infra/ansible/playbooks/dev-builder.yaml @@ -0,0 +1,14 @@ +--- +# Install Docker + Buildx on target host for fast dev image builds. +# +# Usage: +# DEPLOY_HOST= ansible-playbook playbooks/dev-builder.yaml -e ansible_user=ubuntu + +- name: Install dev build tooling on target host + hosts: all + become: yes + gather_facts: yes + + roles: + - role: dev-builder + tags: [dev-builder, docker, dev-tools] diff --git a/infra/ansible/playbooks/dev-loop.yaml b/infra/ansible/playbooks/dev-loop.yaml new file mode 100644 index 00000000..8e25c3d9 --- /dev/null +++ b/infra/ansible/playbooks/dev-loop.yaml @@ -0,0 +1,419 @@ +--- +# Fast developer loop (backend + agent only) +# +# Purpose: +# - Build control-plane/agent images on the target host +# - Import them into k3s containerd (no GHCR push/pull) +# - Patch only runtime workloads for fast iteration +# - Verify rollout and image wiring +# +# Usage: +# # Full loop +# DEPLOY_HOST= ansible-playbook playbooks/dev-loop.yaml -e ansible_user=ubuntu +# +# # Build only +# DEPLOY_HOST= ansible-playbook playbooks/dev-loop.yaml --tags dev-build -e ansible_user=ubuntu +# +# # Deploy only (use previously built/imported images) +# DEPLOY_HOST= ansible-playbook playbooks/dev-loop.yaml --tags dev-deploy -e ansible_user=ubuntu \ +# -e control_plane_image=netclode-control-plane:dev-123 \ +# -e agent_image=netclode-agent:dev-123 +# +# # Verify only +# DEPLOY_HOST= ansible-playbook playbooks/dev-loop.yaml --tags dev-verify -e ansible_user=ubuntu +# +# # Optional: override rsync SSH target explicitly (default: ansible_host/inventory host) +# DEPLOY_HOST= ansible-playbook playbooks/dev-loop.yaml \ +# -e sync_ssh_target=ubuntu@ + +- name: Fast dev loop for backend and agent + hosts: all + become: yes + gather_facts: no + + vars: + dev_namespace: "{{ k8s_namespace | default('netclode') }}" + dev_control_plane_deployment: "{{ control_plane_deployment_name | default('control-plane') }}" + dev_sandbox_template: "{{ sandbox_template_name | default('netclode-agent') }}" + dev_warm_pool_name: "{{ warm_pool_name_override | default('netclode-agent-pool') }}" + + dev_remote_dir: "{{ remote_dir | default('/tmp/netclode-dev-worktree') }}" + + build_control_plane_enabled: "{{ build_control_plane | default(true) | bool }}" + build_agent_enabled: "{{ build_agent | default(true) | bool }}" + import_to_k3s_enabled: "{{ import_to_k3s | default(true) | bool }}" + refresh_warm_pool_enabled: "{{ refresh_warm_pool | default(true) | bool }}" + + pre_tasks: + - name: Resolve dev tag + ansible.builtin.set_fact: + dev_tag: "{{ tag | default('dev-' ~ lookup('pipe', 'date +%Y%m%d-%H%M%S')) }}" + tags: [always] + + - name: Resolve image names + ansible.builtin.set_fact: + resolved_control_plane_image: "{{ control_plane_image | default('netclode-control-plane:' ~ dev_tag) }}" + resolved_agent_image: "{{ agent_image | default('netclode-agent:' ~ dev_tag) }}" + tags: [always] + + - name: Resolve SSH target for rsync sync tasks + ansible.builtin.set_fact: + dev_sync_ssh_target: "{{ sync_ssh_target | default(hostvars[inventory_hostname].ansible_host | default(inventory_hostname)) }}" + tags: [always] + + - name: Show resolved dev loop configuration + ansible.builtin.debug: + msg: + - "dev_tag={{ dev_tag }}" + - "control_plane_image={{ resolved_control_plane_image }}" + - "agent_image={{ resolved_agent_image }}" + - "sync_target={{ dev_sync_ssh_target }}" + - "remote_dir={{ dev_remote_dir }}" + tags: [always] + + tasks: + - name: Prepare remote workspace directories + ansible.builtin.file: + path: "{{ item }}" + state: directory + mode: "0755" + loop: + - "{{ dev_remote_dir }}" + - "{{ dev_remote_dir }}/services" + - "{{ dev_remote_dir }}/services/control-plane" + - "{{ dev_remote_dir }}/services/agent" + tags: [dev-build] + + - name: Ensure remote workspace is writable by SSH user + ansible.builtin.file: + path: "{{ dev_remote_dir }}" + owner: "{{ ansible_user }}" + group: "{{ ansible_user }}" + recurse: yes + when: ansible_user is defined and ansible_user | length > 0 + tags: [dev-build] + + - name: Sync control-plane sources (Docker-required only) + ansible.builtin.command: + argv: + - rsync + - -az + - --delete + - --exclude=.DS_Store + - "{{ playbook_dir }}/../../../services/control-plane/" + - "{{ dev_sync_ssh_target }}:{{ dev_remote_dir }}/services/control-plane/" + delegate_to: localhost + become: no + when: build_control_plane_enabled + tags: [dev-build] + + - name: Sync agent sources (Docker-required only) + ansible.builtin.command: + argv: + - rsync + - -az + - --delete + - --exclude=.DS_Store + - --exclude=node_modules + - --exclude=dist + - "{{ playbook_dir }}/../../../services/agent/" + - "{{ dev_sync_ssh_target }}:{{ dev_remote_dir }}/services/agent/" + delegate_to: localhost + become: no + when: build_agent_enabled + tags: [dev-build] + + - name: Check docker availability on remote host (dev-build preflight) + ansible.builtin.command: + argv: [bash, -lc, "command -v docker"] + register: docker_check + changed_when: false + failed_when: false + when: build_control_plane_enabled or build_agent_enabled + tags: [dev-build] + + - name: Fail when docker is missing on remote host + ansible.builtin.fail: + msg: >- + docker is required on {{ inventory_hostname }} for dev-build, but was not found in PATH. + Install Docker with buildx support on the host, or skip dev-build and run only dev-deploy/dev-verify. + when: + - (build_control_plane_enabled or build_agent_enabled) + - docker_check.rc != 0 + tags: [dev-build] + + - name: Build control-plane image on remote host + ansible.builtin.command: + argv: + - docker + - buildx + - build + - --platform + - linux/amd64 + - -f + - services/control-plane/Dockerfile + - -t + - "{{ resolved_control_plane_image }}" + - --load + - services/control-plane + args: + chdir: "{{ dev_remote_dir }}" + when: build_control_plane_enabled + tags: [dev-build] + + - name: Import control-plane image into k3s containerd + ansible.builtin.shell: | + set -euo pipefail + docker save {{ resolved_control_plane_image | quote }} | k3s ctr images import - + args: + executable: /bin/bash + when: + - build_control_plane_enabled + - import_to_k3s_enabled + tags: [dev-build] + + - name: Build agent image on remote host + ansible.builtin.command: + argv: + - docker + - buildx + - build + - --platform + - linux/amd64 + - -f + - services/agent/Dockerfile + - -t + - "{{ resolved_agent_image }}" + - --load + - . + args: + chdir: "{{ dev_remote_dir }}" + when: build_agent_enabled + tags: [dev-build] + + - name: Import agent image into k3s containerd + ansible.builtin.shell: | + set -euo pipefail + docker save {{ resolved_agent_image | quote }} | k3s ctr images import - + args: + executable: /bin/bash + when: + - build_agent_enabled + - import_to_k3s_enabled + tags: [dev-build] + + - name: Update control-plane deployment image + ansible.builtin.command: + argv: + - k3s + - kubectl + - -n + - "{{ dev_namespace }}" + - set + - image + - "deployment/{{ dev_control_plane_deployment }}" + - "control-plane={{ resolved_control_plane_image }}" + tags: [dev-deploy] + + - name: Update control-plane AGENT_IMAGE env + ansible.builtin.command: + argv: + - k3s + - kubectl + - -n + - "{{ dev_namespace }}" + - set + - env + - "deployment/{{ dev_control_plane_deployment }}" + - "AGENT_IMAGE={{ resolved_agent_image }}" + tags: [dev-deploy] + + - name: Set control-plane imagePullPolicy to IfNotPresent + ansible.builtin.command: + argv: + - k3s + - kubectl + - -n + - "{{ dev_namespace }}" + - patch + - "deployment/{{ dev_control_plane_deployment }}" + - --type=json + - -p + - >- + [{"op":"replace","path":"/spec/template/spec/containers/0/imagePullPolicy","value":"IfNotPresent"}] + tags: [dev-deploy] + + - name: Patch sandbox template image and pull policy + ansible.builtin.command: + argv: + - k3s + - kubectl + - -n + - "{{ dev_namespace }}" + - patch + - "sandboxtemplate/{{ dev_sandbox_template }}" + - --type=json + - -p + - >- + [{"op":"replace","path":"/spec/podTemplate/spec/containers/0/image","value":"{{ resolved_agent_image }}"}, + {"op":"replace","path":"/spec/podTemplate/spec/containers/0/imagePullPolicy","value":"IfNotPresent"}] + tags: [dev-deploy] + + - name: Wait for control-plane rollout + ansible.builtin.command: + argv: + - k3s + - kubectl + - -n + - "{{ dev_namespace }}" + - rollout + - status + - "deployment/{{ dev_control_plane_deployment }}" + - --timeout=180s + tags: [dev-deploy, dev-verify] + + - name: Read current warm pool replicas + ansible.builtin.command: + argv: + - k3s + - kubectl + - -n + - "{{ dev_namespace }}" + - get + - "sandboxwarmpool/{{ dev_warm_pool_name }}" + - -o + - jsonpath={.spec.replicas} + register: warm_pool_replicas_raw + changed_when: false + when: refresh_warm_pool_enabled + tags: [dev-deploy] + + - name: Resolve warm pool replicas fallback + ansible.builtin.set_fact: + warm_pool_replicas: "{{ warm_pool_replicas_raw.stdout | trim | default('1', true) }}" + when: refresh_warm_pool_enabled + tags: [dev-deploy] + + - name: Scale warm pool down to force refreshed warm pods + ansible.builtin.command: + argv: + - k3s + - kubectl + - -n + - "{{ dev_namespace }}" + - patch + - "sandboxwarmpool/{{ dev_warm_pool_name }}" + - --type=merge + - -p + - '{"spec":{"replicas":0}}' + when: refresh_warm_pool_enabled + tags: [dev-deploy] + + - name: Wait briefly for warm pod drain + ansible.builtin.pause: + seconds: 3 + when: refresh_warm_pool_enabled + tags: [dev-deploy] + + - name: Scale warm pool back to previous replica count + ansible.builtin.command: + argv: + - k3s + - kubectl + - -n + - "{{ dev_namespace }}" + - patch + - "sandboxwarmpool/{{ dev_warm_pool_name }}" + - --type=merge + - -p + - "{\"spec\":{\"replicas\":{{ warm_pool_replicas | int }}}}" + when: refresh_warm_pool_enabled + tags: [dev-deploy] + + - name: Read deployed control-plane image + ansible.builtin.command: + argv: + - k3s + - kubectl + - -n + - "{{ dev_namespace }}" + - get + - "deployment/{{ dev_control_plane_deployment }}" + - -o + - jsonpath={.spec.template.spec.containers[0].image} + register: verify_control_plane_image + changed_when: false + tags: [dev-verify] + + - name: Read deployed control-plane AGENT_IMAGE env + ansible.builtin.command: + argv: + - k3s + - kubectl + - -n + - "{{ dev_namespace }}" + - get + - "deployment/{{ dev_control_plane_deployment }}" + - -o + - jsonpath={.spec.template.spec.containers[0].env[?(@.name=="AGENT_IMAGE")].value} + register: verify_agent_image_env + changed_when: false + tags: [dev-verify] + + - name: Read sandbox template image + ansible.builtin.command: + argv: + - k3s + - kubectl + - -n + - "{{ dev_namespace }}" + - get + - "sandboxtemplate/{{ dev_sandbox_template }}" + - -o + - jsonpath={.spec.podTemplate.spec.containers[0].image} + register: verify_sandbox_template_image + changed_when: false + tags: [dev-verify] + + - name: Read sandbox template imagePullPolicy + ansible.builtin.command: + argv: + - k3s + - kubectl + - -n + - "{{ dev_namespace }}" + - get + - "sandboxtemplate/{{ dev_sandbox_template }}" + - -o + - jsonpath={.spec.podTemplate.spec.containers[0].imagePullPolicy} + register: verify_sandbox_pull_policy + changed_when: false + tags: [dev-verify] + + - name: Show verification summary + ansible.builtin.debug: + msg: + - "control-plane image: {{ verify_control_plane_image.stdout }}" + - "control-plane AGENT_IMAGE: {{ verify_agent_image_env.stdout }}" + - "sandbox template image: {{ verify_sandbox_template_image.stdout }}" + - "sandbox template imagePullPolicy: {{ verify_sandbox_pull_policy.stdout }}" + tags: [dev-verify] + + - name: Show recent control-plane logs + ansible.builtin.command: + argv: + - k3s + - kubectl + - -n + - "{{ dev_namespace }}" + - logs + - "deployment/{{ dev_control_plane_deployment }}" + - --tail=60 + register: control_plane_logs + changed_when: false + tags: [dev-verify] + + - name: Print recent control-plane logs + ansible.builtin.debug: + msg: "{{ control_plane_logs.stdout_lines }}" + tags: [dev-verify] diff --git a/infra/ansible/playbooks/k8s-only.yaml b/infra/ansible/playbooks/k8s-only.yaml index 3c19673d..c4b3108d 100644 --- a/infra/ansible/playbooks/k8s-only.yaml +++ b/infra/ansible/playbooks/k8s-only.yaml @@ -7,7 +7,9 @@ - name: Deploy k8s manifests hosts: all become: yes - gather_facts: no + gather_facts: yes + vars_files: + - ../group_vars/all.yaml roles: - role: k8s-manifests diff --git a/infra/ansible/playbooks/secrets.yaml b/infra/ansible/playbooks/secrets.yaml index e68a1134..81ba4d61 100644 --- a/infra/ansible/playbooks/secrets.yaml +++ b/infra/ansible/playbooks/secrets.yaml @@ -6,7 +6,7 @@ # ENV_FILE=/path/to/.env ansible-playbook playbooks/secrets.yaml # # Required in .env: -# ANTHROPIC_API_KEY=sk-ant-... +# At least one API key (ANTHROPIC_API_KEY / OPENAI_API_KEY / MISTRAL_API_KEY / OPENCODE_API_KEY / ZAI_API_KEY) # SSH_AUTHORIZED_KEYS=ssh-ed25519 AAAA... user@host # TS_OAUTH_CLIENT_ID=... # TS_OAUTH_CLIENT_SECRET=... @@ -17,6 +17,7 @@ # # Optional in .env: # TAILSCALE_AUTHKEY=tskey-auth-xxx (for control-plane tsnet - persists in k8s PVC) +# CODEX_OAUTH_ENCRYPTION_KEY_B64=... (required only if using Codex OAuth models) # # Note: K8s secrets require the netclode namespace to exist. # Run site.yaml first, or this will only deploy host secrets. diff --git a/infra/ansible/playbooks/site.yaml b/infra/ansible/playbooks/site.yaml index c397bc48..27919ee8 100644 --- a/infra/ansible/playbooks/site.yaml +++ b/infra/ansible/playbooks/site.yaml @@ -36,6 +36,10 @@ - role: nftables tags: [nftables, firewall, base] + - role: minio + tags: [minio, storage] + when: minio_enabled | default(false) + # Host secrets (before tailscale needs authkey) - role: deploy-secrets tags: [secrets, base] diff --git a/infra/ansible/roles/cilium/tasks/main.yaml b/infra/ansible/roles/cilium/tasks/main.yaml index 4a36c922..e1d43e2e 100644 --- a/infra/ansible/roles/cilium/tasks/main.yaml +++ b/infra/ansible/roles/cilium/tasks/main.yaml @@ -2,6 +2,25 @@ # Cilium CNI role - installs Cilium for NetworkPolicy support # Replaces Flannel (k3s must be started with --flannel-backend=none) +- name: Check if Helm is installed + ansible.builtin.stat: + path: /usr/local/bin/helm + register: cilium_helm_binary + +- name: Download Helm installer + ansible.builtin.get_url: + url: https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 + dest: /tmp/get-helm.sh + mode: "0755" + when: not cilium_helm_binary.stat.exists + +- name: Install Helm + ansible.builtin.command: /tmp/get-helm.sh + args: + creates: /usr/local/bin/helm + environment: + HELM_INSTALL_DIR: /usr/local/bin + - name: Add Cilium Helm repository kubernetes.core.helm_repository: name: cilium diff --git a/infra/ansible/roles/deploy-secrets/tasks/main.yaml b/infra/ansible/roles/deploy-secrets/tasks/main.yaml index 23196893..17694be8 100644 --- a/infra/ansible/roles/deploy-secrets/tasks/main.yaml +++ b/infra/ansible/roles/deploy-secrets/tasks/main.yaml @@ -29,30 +29,31 @@ - name: Extract secrets from .env ansible.builtin.set_fact: - ssh_keys: "{{ env_content_raw | regex_search('(?m)^SSH_AUTHORIZED_KEYS=(.*)$', '\\1') | first | default('') }}" - ts_oauth_client_id: "{{ env_content_raw | regex_search('(?m)^TS_OAUTH_CLIENT_ID=(.*)$', '\\1') | first | default('') }}" - ts_oauth_client_secret: "{{ env_content_raw | regex_search('(?m)^TS_OAUTH_CLIENT_SECRET=(.*)$', '\\1') | first | default('') }}" - tailscale_authkey: "{{ env_content_raw | regex_search('(?m)^TAILSCALE_AUTHKEY=(.*)$', '\\1') | first | default('') }}" - anthropic_api_key: "{{ env_content_raw | regex_search('(?m)^ANTHROPIC_API_KEY=(.*)$', '\\1') | first | default('') }}" - do_spaces_access_key: "{{ env_content_raw | regex_search('(?m)^DO_SPACES_ACCESS_KEY=(.*)$', '\\1') | first | default('') }}" - do_spaces_secret_key: "{{ env_content_raw | regex_search('(?m)^DO_SPACES_SECRET_KEY=(.*)$', '\\1') | first | default('') }}" - juicefs_bucket: "{{ env_content_raw | regex_search('(?m)^JUICEFS_BUCKET=(.*)$', '\\1') | first | default('') }}" - juicefs_meta_url: "{{ env_content_raw | regex_search('(?m)^JUICEFS_META_URL=(.*)$', '\\1') | first | default('') }}" - github_app_id: "{{ env_content_raw | regex_search('(?m)^GITHUB_APP_ID=(.*)$', '\\1') | first | default('') }}" - github_app_private_key_b64: "{{ env_content_raw | regex_search('(?m)^GITHUB_APP_PRIVATE_KEY_B64=(.*)$', '\\1') | first | default('') }}" - github_installation_id: "{{ env_content_raw | regex_search('(?m)^GITHUB_INSTALLATION_ID=(.*)$', '\\1') | first | default('') }}" - kata_vm_cpus: "{{ env_content_raw | regex_search('(?m)^KATA_VM_CPUS=(.*)$', '\\1') | first | default('4') }}" - kata_vm_memory_mb: "{{ env_content_raw | regex_search('(?m)^KATA_VM_MEMORY_MB=(.*)$', '\\1') | first | default('4096') }}" + ssh_keys: "{{ (env_content_raw | regex_search('(?m)^SSH_AUTHORIZED_KEYS=(.*)$', '\\1') or ['']) | first }}" + ts_oauth_client_id: "{{ (env_content_raw | regex_search('(?m)^TS_OAUTH_CLIENT_ID=(.*)$', '\\1') or ['']) | first }}" + ts_oauth_client_secret: "{{ (env_content_raw | regex_search('(?m)^TS_OAUTH_CLIENT_SECRET=(.*)$', '\\1') or ['']) | first }}" + tailscale_authkey: "{{ (env_content_raw | regex_search('(?m)^TAILSCALE_AUTHKEY=(.*)$', '\\1') or ['']) | first }}" + anthropic_api_key: "{{ (env_content_raw | regex_search('(?m)^ANTHROPIC_API_KEY=(.*)$', '\\1') or ['']) | first }}" + do_spaces_access_key: "{{ (env_content_raw | regex_search('(?m)^DO_SPACES_ACCESS_KEY=(.*)$', '\\1') or ['']) | first }}" + do_spaces_secret_key: "{{ (env_content_raw | regex_search('(?m)^DO_SPACES_SECRET_KEY=(.*)$', '\\1') or ['']) | first }}" + juicefs_bucket: "{{ (env_content_raw | regex_search('(?m)^JUICEFS_BUCKET=(.*)$', '\\1') or ['']) | first }}" + juicefs_meta_url: "{{ (env_content_raw | regex_search('(?m)^JUICEFS_META_URL=(.*)$', '\\1') or ['']) | first }}" + minio_enabled_env: "{{ ((env_content_raw | regex_search('(?m)^MINIO_ENABLED=(.*)$', '\\1') or ['false']) | first | lower) }}" + minio_bucket_name_env: "{{ (env_content_raw | regex_search('(?m)^MINIO_BUCKET_NAME=(.*)$', '\\1') or ['netclode-juicefs']) | first }}" + minio_api_port_env: "{{ (env_content_raw | regex_search('(?m)^MINIO_API_PORT=(.*)$', '\\1') or ['9000']) | first }}" + github_app_id: "{{ (env_content_raw | regex_search('(?m)^GITHUB_APP_ID=(.*)$', '\\1') or ['']) | first }}" + github_app_private_key_b64: "{{ (env_content_raw | regex_search('(?m)^GITHUB_APP_PRIVATE_KEY_B64=(.*)$', '\\1') or ['']) | first }}" + github_installation_id: "{{ (env_content_raw | regex_search('(?m)^GITHUB_INSTALLATION_ID=(.*)$', '\\1') or ['']) | first }}" + kata_vm_cpus: "{{ (env_content_raw | regex_search('(?m)^KATA_VM_CPUS=(.*)$', '\\1') or ['4']) | first }}" + kata_vm_memory_mb: "{{ (env_content_raw | regex_search('(?m)^KATA_VM_MEMORY_MB=(.*)$', '\\1') or ['4096']) | first }}" github_token: "{{ (env_content_raw | regex_search('(?m)^GITHUB_TOKEN=(.*)$', '\\1') or [''])[0] }}" github_copilot_token: "{{ (env_content_raw | regex_search('(?m)^GITHUB_COPILOT_TOKEN=(.*)$', '\\1') or [''])[0] }}" openai_api_key: "{{ (env_content_raw | regex_search('(?m)^OPENAI_API_KEY=(.*)$', '\\1') or [''])[0] }}" - codex_access_token: "{{ (env_content_raw | regex_search('(?m)^CODEX_ACCESS_TOKEN=(.*)$', '\\1') or [''])[0] }}" - codex_id_token: "{{ (env_content_raw | regex_search('(?m)^CODEX_ID_TOKEN=(.*)$', '\\1') or [''])[0] }}" - codex_refresh_token: "{{ (env_content_raw | regex_search('(?m)^CODEX_REFRESH_TOKEN=(.*)$', '\\1') or [''])[0] }}" + codex_oauth_encryption_key_b64: "{{ (env_content_raw | regex_search('(?m)^CODEX_OAUTH_ENCRYPTION_KEY_B64=(.*)$', '\\1') or [''])[0] }}" mistral_api_key: "{{ (env_content_raw | regex_search('(?m)^MISTRAL_API_KEY=(.*)$', '\\1') or [''])[0] }}" opencode_api_key: "{{ (env_content_raw | regex_search('(?m)^OPENCODE_API_KEY=(.*)$', '\\1') or [''])[0] }}" zai_api_key: "{{ (env_content_raw | regex_search('(?m)^ZAI_API_KEY=(.*)$', '\\1') or [''])[0] }}" - max_active_sessions: "{{ env_content_raw | regex_search('(?m)^MAX_ACTIVE_SESSIONS=(.*)$', '\\1') | first | default('5') }}" + max_active_sessions: "{{ (env_content_raw | regex_search('(?m)^MAX_ACTIVE_SESSIONS=(.*)$', '\\1') or ['5']) | first }}" # Host secrets - skip if only deploying k8s secrets - name: Deploy host secrets @@ -123,9 +124,6 @@ | combine({'opencode': opencode_api_key} if opencode_api_key | length > 0 else {}) | combine({'zai': zai_api_key} if zai_api_key | length > 0 else {}) | combine({'github_copilot': github_copilot_token} if github_copilot_token | length > 0 else {}) - | combine({'codex_access': codex_access_token} if codex_access_token | length > 0 else {}) - | combine({'codex_id': codex_id_token} if codex_id_token | length > 0 else {}) - | combine({'codex_refresh': codex_refresh_token} if codex_refresh_token | length > 0 else {}) }} # Create secret-proxy-secrets for the proxy (contains real API keys) @@ -163,9 +161,7 @@ opencode-api-key: "{{ opencode_api_key }}" zai-api-key: "{{ zai_api_key }}" github-copilot-token: "{{ github_copilot_token }}" - codex-access-token: "{{ codex_access_token }}" - codex-id-token: "{{ codex_id_token }}" - codex-refresh-token: "{{ codex_refresh_token }}" + codex-oauth-encryption-key-b64: "{{ codex_oauth_encryption_key_b64 }}" # Placeholders for sandbox (agent sees these, proxy replaces them) anthropic-api-key-placeholder: "NETCLODE_PLACEHOLDER_anthropic" openai-api-key-placeholder: "NETCLODE_PLACEHOLDER_openai" @@ -173,9 +169,6 @@ opencode-api-key-placeholder: "NETCLODE_PLACEHOLDER_opencode" zai-api-key-placeholder: "NETCLODE_PLACEHOLDER_zai" github-copilot-token-placeholder: "NETCLODE_PLACEHOLDER_github_copilot" - codex-access-token-placeholder: "NETCLODE_PLACEHOLDER_codex_access" - codex-id-token-placeholder: "NETCLODE_PLACEHOLDER_codex_id" - codex-refresh-token-placeholder: "NETCLODE_PLACEHOLDER_codex_refresh" # Non-proxied secrets (used directly, not through proxy) # GITHUB_TOKEN is for git operations (clone/push) - uses credential helper, not HTTP headers github-token: "{{ github_token }}" @@ -184,7 +177,6 @@ github-app-private-key: "{{ github_app_private_key_b64 | b64decode }}" github-installation-id: "{{ github_installation_id }}" tailscale-authkey: "{{ tailscale_authkey }}" - when: anthropic_api_key | length > 0 - name: Check if juicefs-secret already exists kubernetes.core.k8s_info: @@ -195,12 +187,72 @@ kubeconfig: "{{ k3s_kubeconfig }}" register: existing_juicefs_secret + - name: Determine if MinIO storage auto-wiring is enabled + ansible.builtin.set_fact: + minio_storage_autowire: "{{ minio_enabled_env == 'true' }}" + + - name: Check for persisted MinIO credential files + ansible.builtin.stat: + path: "{{ item }}" + loop: + - /var/secrets/minio-root-user + - /var/secrets/minio-root-password + register: minio_saved_credential_files + when: + - minio_storage_autowire + - do_spaces_access_key | length == 0 or do_spaces_secret_key | length == 0 + + - name: Read persisted MinIO root user + ansible.builtin.slurp: + src: /var/secrets/minio-root-user + register: minio_saved_root_user + when: + - minio_storage_autowire + - do_spaces_access_key | length == 0 + - minio_saved_credential_files.results[0].stat.exists + + - name: Read persisted MinIO root password + ansible.builtin.slurp: + src: /var/secrets/minio-root-password + register: minio_saved_root_password + when: + - minio_storage_autowire + - do_spaces_secret_key | length == 0 + - minio_saved_credential_files.results[1].stat.exists + + - name: Fill S3 credentials from MinIO persisted credentials + ansible.builtin.set_fact: + do_spaces_access_key: "{{ do_spaces_access_key if do_spaces_access_key | length > 0 else (minio_saved_root_user.content | default('') | b64decode | trim) }}" + do_spaces_secret_key: "{{ do_spaces_secret_key if do_spaces_secret_key | length > 0 else (minio_saved_root_password.content | default('') | b64decode | trim) }}" + when: minio_storage_autowire + + - name: Discover host IPv4 for MinIO endpoint + ansible.builtin.command: sh -c "ip -4 route get 1.1.1.1 | awk '{print $7; exit}'" + register: minio_host_ipv4 + changed_when: false + when: + - minio_storage_autowire + - juicefs_bucket | length == 0 + + - name: Fill JuiceFS bucket from local MinIO endpoint + ansible.builtin.set_fact: + juicefs_bucket: "http://{{ minio_host_ipv4.stdout | trim }}:{{ minio_api_port_env }}/{{ minio_bucket_name_env }}" + when: + - minio_storage_autowire + - juicefs_bucket | length == 0 + + - name: Set default JuiceFS metadata URL + ansible.builtin.set_fact: + juicefs_meta_url: "redis://redis-juicefs.netclode.svc.cluster.local:6379/0" + when: juicefs_meta_url | length == 0 + - name: Generate JuiceFS volume name with random suffix ansible.builtin.set_fact: juicefs_volume_name: "netclode-{{ lookup('password', '/dev/null chars=ascii_lowercase,digits length=6') }}" when: - existing_juicefs_secret.resources | length == 0 - do_spaces_access_key | length > 0 + - do_spaces_secret_key | length > 0 - name: Use existing JuiceFS volume name ansible.builtin.set_fact: @@ -226,4 +278,8 @@ bucket: "{{ juicefs_bucket }}" access-key: "{{ do_spaces_access_key }}" secret-key: "{{ do_spaces_secret_key }}" - when: do_spaces_access_key | length > 0 + when: + - do_spaces_access_key | length > 0 + - do_spaces_secret_key | length > 0 + - juicefs_bucket | length > 0 + - juicefs_meta_url | length > 0 diff --git a/infra/ansible/roles/dev-builder/defaults/main.yaml b/infra/ansible/roles/dev-builder/defaults/main.yaml new file mode 100644 index 00000000..1cd6ed71 --- /dev/null +++ b/infra/ansible/roles/dev-builder/defaults/main.yaml @@ -0,0 +1,6 @@ +--- +dev_builder_packages: + - docker.io + - docker-buildx + +dev_builder_user: "{{ ansible_user | default('ubuntu') }}" diff --git a/infra/ansible/roles/dev-builder/tasks/main.yaml b/infra/ansible/roles/dev-builder/tasks/main.yaml new file mode 100644 index 00000000..1a347350 --- /dev/null +++ b/infra/ansible/roles/dev-builder/tasks/main.yaml @@ -0,0 +1,28 @@ +--- +- name: Install docker builder packages + ansible.builtin.apt: + name: "{{ dev_builder_packages }}" + state: present + update_cache: yes + +- name: Ensure docker service is enabled and running + ansible.builtin.systemd: + name: docker + state: started + enabled: yes + +- name: Ensure dev builder user is in docker group + ansible.builtin.user: + name: "{{ dev_builder_user }}" + groups: docker + append: yes + +- name: Verify docker CLI availability + ansible.builtin.command: + argv: [docker, --version] + changed_when: false + +- name: Verify docker buildx availability + ansible.builtin.command: + argv: [docker, buildx, version] + changed_when: false diff --git a/infra/ansible/roles/k8s-manifests/defaults/main.yaml b/infra/ansible/roles/k8s-manifests/defaults/main.yaml index dc554ba7..fa078d76 100644 --- a/infra/ansible/roles/k8s-manifests/defaults/main.yaml +++ b/infra/ansible/roles/k8s-manifests/defaults/main.yaml @@ -1,3 +1,9 @@ --- # k8s-manifests defaults # The manifest directory is set dynamically in tasks +control_plane_image: "ghcr.io/angristan/netclode-control-plane:latest" +agent_image: "ghcr.io/angristan/netclode-agent:latest" +image_pull_secret_name: "" +image_pull_secret_registry: "ghcr.io" +image_pull_secret_username: "" +image_pull_secret_password: "" diff --git a/infra/ansible/roles/k8s-manifests/tasks/main.yaml b/infra/ansible/roles/k8s-manifests/tasks/main.yaml index 96202904..c750f249 100644 --- a/infra/ansible/roles/k8s-manifests/tasks/main.yaml +++ b/infra/ansible/roles/k8s-manifests/tasks/main.yaml @@ -40,6 +40,41 @@ definition: "{{ lookup('file', k8s_manifest_dir ~ '/namespace.yaml') | from_yaml_all }}" kubeconfig: "{{ k3s_kubeconfig }}" +- name: Create image pull secret (optional) + kubernetes.core.k8s: + state: present + kubeconfig: "{{ k3s_kubeconfig }}" + definition: + apiVersion: v1 + kind: Secret + metadata: + name: "{{ image_pull_secret_name }}" + namespace: netclode + type: kubernetes.io/dockerconfigjson + stringData: + .dockerconfigjson: >- + {{ + { + "auths": { + image_pull_secret_registry: { + "username": image_pull_secret_username, + "password": image_pull_secret_password, + "auth": (image_pull_secret_username ~ ":" ~ image_pull_secret_password) | b64encode + } + } + } | to_json + }} + when: + - image_pull_secret_name | length > 0 + - image_pull_secret_username | length > 0 + - image_pull_secret_password | length > 0 + +- name: Deploy priority classes + kubernetes.core.k8s: + state: present + definition: "{{ lookup('file', k8s_manifest_dir ~ '/priority-classes.yaml') | from_yaml_all }}" + kubeconfig: "{{ k3s_kubeconfig }}" + - name: Deploy RBAC kubernetes.core.k8s: state: present @@ -80,7 +115,7 @@ - name: Deploy sandbox template kubernetes.core.k8s: state: present - definition: "{{ lookup('file', k8s_manifest_dir ~ '/sandbox-template.yaml') | from_yaml_all }}" + definition: "{{ lookup('template', 'sandbox-template.yaml.j2') | from_yaml_all }}" kubeconfig: "{{ k3s_kubeconfig }}" - name: Deploy sandbox warm pool diff --git a/infra/ansible/roles/k8s-manifests/templates/control-plane.yaml.j2 b/infra/ansible/roles/k8s-manifests/templates/control-plane.yaml.j2 index d8048d51..6cae0da6 100644 --- a/infra/ansible/roles/k8s-manifests/templates/control-plane.yaml.j2 +++ b/infra/ansible/roles/k8s-manifests/templates/control-plane.yaml.j2 @@ -17,9 +17,13 @@ spec: app: control-plane spec: serviceAccountName: control-plane +{% if image_pull_secret_name | length > 0 %} + imagePullSecrets: + - name: {{ image_pull_secret_name }} +{% endif %} containers: - name: control-plane - image: ghcr.io/angristan/netclode-control-plane:latest + image: {{ control_plane_image }} ports: - containerPort: 3000 name: http @@ -30,11 +34,14 @@ spec: valueFrom: fieldRef: fieldPath: metadata.namespace + - name: AGENT_IMAGE + value: "{{ agent_image }}" - name: ANTHROPIC_API_KEY valueFrom: secretKeyRef: name: netclode-secrets key: anthropic-api-key + optional: true # GitHub Copilot token for Copilot SDK (optional) - name: GITHUB_COPILOT_TOKEN valueFrom: @@ -67,23 +74,11 @@ spec: name: netclode-secrets key: openai-api-key optional: true - - name: CODEX_ACCESS_TOKEN - valueFrom: - secretKeyRef: - name: netclode-secrets - key: codex-access-token - optional: true - - name: CODEX_ID_TOKEN - valueFrom: - secretKeyRef: - name: netclode-secrets - key: codex-id-token - optional: true - - name: CODEX_REFRESH_TOKEN + - name: CODEX_OAUTH_ENCRYPTION_KEY_B64 valueFrom: secretKeyRef: name: netclode-secrets - key: codex-refresh-token + key: codex-oauth-encryption-key-b64 optional: true - name: MISTRAL_API_KEY valueFrom: diff --git a/infra/ansible/roles/k8s-manifests/templates/sandbox-template.yaml.j2 b/infra/ansible/roles/k8s-manifests/templates/sandbox-template.yaml.j2 new file mode 100644 index 00000000..412a23f2 --- /dev/null +++ b/infra/ansible/roles/k8s-manifests/templates/sandbox-template.yaml.j2 @@ -0,0 +1,239 @@ +# SandboxTemplate for Netclode Agent +# +# Security note on privileged: true +# --------------------------------- +# The agent runs with privileged: true because the image runs Docker-in-Docker +# which needs to mount /proc, /dev, /sys, etc. +# +# This is SAFE because: +# 1. Kata Containers runs the pod inside an isolated VM with its own kernel +# 2. "privileged" only grants root inside the VM, not on the host +# 3. containerd config has privileged_without_host_devices=true, so no host +# devices (/dev/kvm, /dev/sda, etc.) are passed through +# 4. The VM boundary is the security layer, not container namespaces +# +# Secret Proxy Architecture +# ------------------------- +# Two-tier proxy architecture for secure API key injection: +# +# SDK → auth-proxy (localhost:8080) → secret-proxy (external) → internet +# adds SA token injects secrets +# NO secrets HAS secrets +# inside microVM outside microVM +# +# 1. Agent sees placeholder values (e.g., ANTHROPIC_API_KEY=NETCLODE_PLACEHOLDER_xxx) +# 2. HTTP_PROXY points to local auth-proxy (localhost:8080), which runs inside the sandbox +# 3. auth-proxy reads the mounted K8s ServiceAccount token and adds Proxy-Authorization header +# 4. auth-proxy forwards to external secret-proxy service +# 5. secret-proxy validates token with control-plane (checks session allowlist by SDK type) +# 6. secret-proxy replaces placeholders with real secrets ONLY for allowed hosts +# +# Security: +# - Real secrets NEVER enter the sandbox microVM +# - Per-session authorization (Claude session can't use Mistral API) +# - Token-based auth, not IP-based (cryptographic identity) +# - Control-plane is the single source of truth for permissions +# +# (Inspired by Fly's Tokenizer: https://github.com/superfly/tokenizer) +# +apiVersion: extensions.agents.x-k8s.io/v1alpha1 +kind: SandboxTemplate +metadata: + name: netclode-agent + namespace: netclode +spec: + podTemplate: + spec: + runtimeClassName: kata-clh +{% if image_pull_secret_name | length > 0 %} + imagePullSecrets: + - name: {{ image_pull_secret_name }} +{% endif %} + initContainers: + # Fix MTU for Cilium + Kata compatibility + # Kata VMs don't inherit route MTU from outer namespace, causing fragmentation + # See: https://docs.cilium.io/en/stable/network/kubernetes/kata.html#limitations + - name: fix-mtu + image: busybox:latest + command: + - sh + - -c + - | + # Extract gateway and interface from default route + GW=$(ip route | grep "^default" | awk '{print $3}') + DEV=$(ip route | grep "^default" | awk '{print $5}') + if [ -n "$GW" ] && [ -n "$DEV" ]; then + ip route change default via "$GW" dev "$DEV" mtu 1450 || true + fi + securityContext: + capabilities: + add: + - NET_ADMIN + containers: + - name: agent + image: {{ agent_image }} + imagePullPolicy: Always + securityContext: + privileged: true # See security note above + env: + - name: NODE_ENV + value: "production" + # Proxy settings - local auth-proxy adds SA token, forwards to external secret-proxy + # SDK → localhost:8080 (auth-proxy) → secret-proxy.svc:8080 → internet + - name: HTTP_PROXY + value: "http://127.0.0.1:8080" + - name: HTTPS_PROXY + value: "http://127.0.0.1:8080" + - name: NO_PROXY + value: "localhost,127.0.0.1,control-plane.netclode.svc.cluster.local" + # External secret-proxy URL (auth-proxy forwards to this) + - name: SECRET_PROXY_URL + value: "http://secret-proxy.netclode.svc.cluster.local:8080" + # Agent sees PLACEHOLDERS for API keys (real secrets injected by external proxy) + - name: ANTHROPIC_API_KEY + valueFrom: + secretKeyRef: + name: netclode-secrets + key: anthropic-api-key-placeholder + - name: OPENAI_API_KEY + valueFrom: + secretKeyRef: + name: netclode-secrets + key: openai-api-key-placeholder + optional: true + - name: MISTRAL_API_KEY + valueFrom: + secretKeyRef: + name: netclode-secrets + key: mistral-api-key-placeholder + optional: true + - name: OPENCODE_API_KEY + valueFrom: + secretKeyRef: + name: netclode-secrets + key: opencode-api-key-placeholder + optional: true + - name: ZAI_API_KEY + valueFrom: + secretKeyRef: + name: netclode-secrets + key: zai-api-key-placeholder + optional: true + - name: GITHUB_COPILOT_TOKEN + valueFrom: + secretKeyRef: + name: netclode-secrets + key: github-copilot-token-placeholder + optional: true + # GITHUB_TOKEN is NOT proxied - used for git credential helper (clone/push) + # Git embeds credentials in URL, doesn't use HTTP Authorization headers + - name: GITHUB_TOKEN + valueFrom: + secretKeyRef: + name: netclode-secrets + key: github-token + optional: true + - name: CONTROL_PLANE_URL + value: "http://control-plane.netclode.svc.cluster.local" + - name: POD_NAME + valueFrom: + fieldRef: + fieldPath: metadata.name + volumeMounts: + - name: agent-home + mountPath: /agent + # Mount proxy CA so agent trusts MITM certificates from external proxy + - name: proxy-ca + mountPath: /usr/local/share/ca-certificates/secret-proxy.crt + subPath: ca.crt + readOnly: true + # Mount ServiceAccount token for proxy authentication + - name: proxy-auth-token + mountPath: /var/run/secrets/proxy-auth + readOnly: true + # Agent connects TO control-plane, no ports exposed + # Readiness: check for ready file created by agent + readinessProbe: + exec: + command: ["test", "-f", "/tmp/agent-ready"] + initialDelaySeconds: 3 + periodSeconds: 5 + livenessProbe: + exec: + command: ["test", "-f", "/tmp/agent-ready"] + initialDelaySeconds: 30 + periodSeconds: 10 + volumes: + # ConfigMap containing proxy CA certificate (for HTTPS MITM trust) + - name: proxy-ca + configMap: + name: secret-proxy-ca + # Projected ServiceAccount token for proxy authentication + - name: proxy-auth-token + projected: + sources: + - serviceAccountToken: + path: token + expirationSeconds: 3600 + audience: secret-proxy + volumeClaimTemplates: + - metadata: + name: agent-home + spec: + accessModes: ["ReadWriteOnce"] + storageClassName: juicefs-sc + resources: + requests: + storage: 10Gi + networkPolicy: + # Ingress: Allow Tailscale proxy for preview URLs (agent web servers) + ingress: + - from: + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: tailscale + ports: + - protocol: TCP + port: 3002 + - protocol: TCP + port: 8080 + egress: + # Allow connection to control-plane (port 80 -> 3000) + - to: + - podSelector: + matchLabels: + app: control-plane + ports: + - protocol: TCP + port: 80 + - protocol: TCP + port: 3000 + # Allow connection to secret-proxy (for API key injection) + - to: + - podSelector: + matchLabels: + app: secret-proxy + ports: + - protocol: TCP + port: 8080 + # Allow DNS + - to: + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: kube-system + ports: + - protocol: TCP + port: 53 + - protocol: UDP + port: 53 + # Allow connection to Ollama for local GPU inference (if deployed) + - to: + - podSelector: + matchLabels: + app: ollama + ports: + - protocol: TCP + port: 11434 + # NOTE: Internet access (0.0.0.0/0) is enabled by default via per-session + # NetworkPolicy added by control-plane (public internet; private ranges blocked). + # See docs/network-access.md. diff --git a/infra/ansible/roles/minio/defaults/main.yaml b/infra/ansible/roles/minio/defaults/main.yaml new file mode 100644 index 00000000..fd975555 --- /dev/null +++ b/infra/ansible/roles/minio/defaults/main.yaml @@ -0,0 +1,15 @@ +--- +# MinIO defaults +minio_user: minio +minio_group: minio +minio_data_dir: /var/lib/minio/data +minio_config_dir: /etc/minio +minio_bind_address: "0.0.0.0" +minio_console_bind_address: "0.0.0.0" +minio_scheme: http +minio_binary_url: https://dl.min.io/server/minio/release/linux-amd64/minio +minio_client_url: https://dl.min.io/client/mc/release/linux-amd64/mc +# If set explicitly, these override values read from .env. +minio_root_user: "" +minio_root_password: "" + diff --git a/infra/ansible/roles/minio/tasks/main.yaml b/infra/ansible/roles/minio/tasks/main.yaml new file mode 100644 index 00000000..f4ac141b --- /dev/null +++ b/infra/ansible/roles/minio/tasks/main.yaml @@ -0,0 +1,273 @@ +--- +# MinIO role - self-hosted S3-compatible object storage + +- name: Skip if MinIO disabled + ansible.builtin.debug: + msg: "MinIO disabled via minio_enabled={{ minio_enabled }}" + when: not minio_enabled + +- name: End play if MinIO disabled + ansible.builtin.meta: end_host + when: not minio_enabled + +- name: Check nftables service state + ansible.builtin.command: systemctl is-active nftables + register: minio_nftables_state + changed_when: false + failed_when: false + +- name: Require firewall before exposing MinIO listener + ansible.builtin.assert: + that: + - minio_nftables_state.rc == 0 + fail_msg: | + nftables must be active before deploying MinIO with network listener. + Run: ansible-playbook playbooks/site.yaml --tags "nftables,minio" + +- name: Set .env file path (defaults to repo root) + ansible.builtin.set_fact: + minio_env_file: "{{ (playbook_dir + '/../../..') | realpath }}/.env" + +- name: Check if .env file exists + ansible.builtin.stat: + path: "{{ minio_env_file }}" + delegate_to: localhost + become: no + register: minio_env_file_stat + +- name: Read .env file content + ansible.builtin.set_fact: + minio_env_content_raw: "{{ lookup('file', minio_env_file) }}" + delegate_to: localhost + become: no + when: minio_env_file_stat.stat.exists + +- name: Extract MinIO fallback credentials from .env + ansible.builtin.set_fact: + env_do_spaces_access_key: "{{ (minio_env_content_raw | regex_search('(?m)^DO_SPACES_ACCESS_KEY=(.*)$', '\\1') or ['']) | first }}" + env_do_spaces_secret_key: "{{ (minio_env_content_raw | regex_search('(?m)^DO_SPACES_SECRET_KEY=(.*)$', '\\1') or ['']) | first }}" + when: minio_env_file_stat.stat.exists + +- name: Check for persisted MinIO credentials + ansible.builtin.stat: + path: "{{ item }}" + loop: + - /var/secrets/minio-root-user + - /var/secrets/minio-root-password + register: minio_saved_credential_files + +- name: Read persisted MinIO root user + ansible.builtin.slurp: + src: /var/secrets/minio-root-user + register: minio_saved_root_user + when: + - minio_saved_credential_files.results[0].stat.exists + - minio_saved_credential_files.results[1].stat.exists + +- name: Read persisted MinIO root password + ansible.builtin.slurp: + src: /var/secrets/minio-root-password + register: minio_saved_root_password + when: + - minio_saved_credential_files.results[0].stat.exists + - minio_saved_credential_files.results[1].stat.exists + +- name: Determine if .env fallback credentials are valid + ansible.builtin.set_fact: + minio_env_credentials_valid: >- + {{ + (env_do_spaces_access_key | default('') | length > 2) + and (env_do_spaces_secret_key | default('') | length > 7) + and (env_do_spaces_access_key | default('') != 'your-spaces-access-key') + and (env_do_spaces_secret_key | default('') != 'your-spaces-secret-key') + }} + +- name: Resolve MinIO credentials from explicit vars, persisted secrets, or .env + ansible.builtin.set_fact: + minio_effective_root_user: >- + {{ + minio_root_user + if minio_root_user | length > 0 else + ((minio_saved_root_user.content | b64decode | trim) + if (minio_saved_credential_files.results[0].stat.exists and minio_saved_credential_files.results[1].stat.exists) else + ((env_do_spaces_access_key | default('')) if minio_env_credentials_valid else '') + ) + }} + minio_effective_root_password: >- + {{ + minio_root_password + if minio_root_password | length > 0 else + ((minio_saved_root_password.content | b64decode | trim) + if (minio_saved_credential_files.results[0].stat.exists and minio_saved_credential_files.results[1].stat.exists) else + ((env_do_spaces_secret_key | default('')) if minio_env_credentials_valid else '') + ) + }} + minio_effective_bucket_name: "{{ minio_bucket_name | trim }}" + minio_access_key_source: >- + {{ + 'minio_root_user variable' + if minio_root_user | length > 0 else + ('persisted /var/secrets credentials' + if (minio_saved_credential_files.results[0].stat.exists and minio_saved_credential_files.results[1].stat.exists) else + ('.env DO_SPACES_ACCESS_KEY' + if minio_env_credentials_valid else + 'generated' + ) + ) + }} + +- name: Generate MinIO root user when no credentials are available + ansible.builtin.shell: "printf 'minio-%s' \"$(tr -dc 'a-z0-9' 0 else (minio_generated_root_user.stdout | trim) }}" + minio_effective_root_password: "{{ minio_effective_root_password if minio_effective_root_password | length > 0 else (minio_generated_root_password.stdout | trim) }}" + minio_access_key_source: generated + when: + - minio_effective_root_user | length == 0 or minio_effective_root_password | length == 0 + +- name: Ensure /var/secrets exists + ansible.builtin.file: + path: /var/secrets + state: directory + owner: root + group: root + mode: "0700" + +- name: Persist MinIO root user + ansible.builtin.copy: + content: "{{ minio_effective_root_user }}" + dest: /var/secrets/minio-root-user + owner: root + group: root + mode: "0600" + +- name: Persist MinIO root password + ansible.builtin.copy: + content: "{{ minio_effective_root_password }}" + dest: /var/secrets/minio-root-password + owner: root + group: root + mode: "0600" + +- name: Validate MinIO credentials + ansible.builtin.assert: + that: + - minio_effective_root_user | length > 2 + - minio_effective_root_password | length > 7 + fail_msg: | + Missing MinIO credentials. + Set MINIO_ROOT_USER / MINIO_ROOT_PASSWORD as Ansible vars, or provide + DO_SPACES_ACCESS_KEY / DO_SPACES_SECRET_KEY in .env for fallback. + +- name: Install MinIO prerequisites + ansible.builtin.apt: + name: + - ca-certificates + - curl + state: present + update_cache: yes + +- name: Create MinIO group + ansible.builtin.group: + name: "{{ minio_group }}" + system: yes + state: present + +- name: Create MinIO user + ansible.builtin.user: + name: "{{ minio_user }}" + group: "{{ minio_group }}" + system: yes + shell: /usr/sbin/nologin + home: "{{ minio_data_dir }}" + create_home: no + state: present + +- name: Create MinIO directories + ansible.builtin.file: + path: "{{ item.path }}" + state: directory + owner: "{{ minio_user }}" + group: "{{ minio_group }}" + mode: "{{ item.mode }}" + loop: + - { path: "{{ minio_data_dir }}", mode: "0750" } + - { path: "{{ minio_config_dir }}", mode: "0700" } + +- name: Download MinIO server binary + ansible.builtin.get_url: + url: "{{ minio_binary_url }}" + dest: /usr/local/bin/minio + mode: "0755" + owner: root + group: root + +- name: Download MinIO client binary + ansible.builtin.get_url: + url: "{{ minio_client_url }}" + dest: /usr/local/bin/mc + mode: "0755" + owner: root + group: root + +- name: Deploy MinIO environment file + ansible.builtin.template: + src: minio.env.j2 + dest: "{{ minio_config_dir }}/minio.env" + owner: root + group: root + mode: "0600" + register: minio_env_template + +- name: Deploy MinIO systemd service + ansible.builtin.template: + src: minio.service.j2 + dest: /etc/systemd/system/minio.service + owner: root + group: root + mode: "0644" + register: minio_service_template + +- name: Enable and start MinIO service + ansible.builtin.systemd: + name: minio + daemon_reload: yes + enabled: yes + state: "{{ 'restarted' if (minio_env_template.changed or minio_service_template.changed) else 'started' }}" + +- name: Wait for MinIO API port + ansible.builtin.wait_for: + host: 127.0.0.1 + port: "{{ minio_api_port | int }}" + timeout: 30 + +- name: Configure MinIO local alias + ansible.builtin.command: > + /usr/local/bin/mc alias set local + {{ minio_scheme }}://127.0.0.1:{{ minio_api_port }} + {{ minio_effective_root_user }} + {{ minio_effective_root_password }} + changed_when: false + +- name: Ensure JuiceFS bucket exists in MinIO + ansible.builtin.command: "/usr/local/bin/mc mb --ignore-existing local/{{ minio_effective_bucket_name }}" + changed_when: false + +- name: Show MinIO integration values + ansible.builtin.debug: + msg: | + MinIO API: {{ minio_scheme }}://{{ ansible_facts['default_ipv4']['address'] }}:{{ minio_api_port }} + MinIO Console: http://{{ ansible_facts['default_ipv4']['address'] }}:{{ minio_console_port }} + JuiceFS bucket URL: {{ minio_scheme }}://{{ ansible_facts['default_ipv4']['address'] }}:{{ minio_api_port }}/{{ minio_effective_bucket_name }} + Access key source: {{ minio_access_key_source }} diff --git a/infra/ansible/roles/minio/templates/minio.env.j2 b/infra/ansible/roles/minio/templates/minio.env.j2 new file mode 100644 index 00000000..ba787c9b --- /dev/null +++ b/infra/ansible/roles/minio/templates/minio.env.j2 @@ -0,0 +1,4 @@ +MINIO_ROOT_USER="{{ minio_effective_root_user }}" +MINIO_ROOT_PASSWORD="{{ minio_effective_root_password }}" +MINIO_VOLUMES="{{ minio_data_dir }}" +MINIO_OPTS="--address {{ minio_bind_address }}:{{ minio_api_port }} --console-address {{ minio_console_bind_address }}:{{ minio_console_port }}" diff --git a/infra/ansible/roles/minio/templates/minio.service.j2 b/infra/ansible/roles/minio/templates/minio.service.j2 new file mode 100644 index 00000000..351df367 --- /dev/null +++ b/infra/ansible/roles/minio/templates/minio.service.j2 @@ -0,0 +1,22 @@ +[Unit] +Description=MinIO +Documentation=https://min.io/docs/minio/linux/index.html +Wants=network-online.target +After=network-online.target +AssertFileIsExecutable=/usr/local/bin/minio + +[Service] +Type=notify +User={{ minio_user }} +Group={{ minio_group }} +WorkingDirectory={{ minio_data_dir }} +EnvironmentFile={{ minio_config_dir }}/minio.env +ExecStart=/usr/local/bin/minio server $MINIO_OPTS $MINIO_VOLUMES +Restart=always +LimitNOFILE=65536 +TasksMax=infinity +TimeoutStopSec=infinity +SendSIGKILL=no + +[Install] +WantedBy=multi-user.target diff --git a/infra/k8s/namespace.yaml b/infra/k8s/namespace.yaml index 26a3e650..63888de8 100644 --- a/infra/k8s/namespace.yaml +++ b/infra/k8s/namespace.yaml @@ -38,6 +38,11 @@ rules: - apiGroups: ["networking.k8s.io"] resources: ["networkpolicies"] verbs: ["get", "list", "watch", "create", "update", "delete"] + # Read control-plane ingress hostname to construct full preview URLs + - apiGroups: ["networking.k8s.io"] + resources: ["ingresses"] + resourceNames: ["control-plane"] + verbs: ["get"] - apiGroups: [""] resources: ["persistentvolumeclaims"] verbs: ["get", "list", "watch", "create", "update", "patch", "delete"] diff --git a/infra/k8s/sandbox-template.yaml b/infra/k8s/sandbox-template.yaml index a36aa6a7..8cbf1376 100644 --- a/infra/k8s/sandbox-template.yaml +++ b/infra/k8s/sandbox-template.yaml @@ -121,24 +121,6 @@ spec: name: netclode-secrets key: github-copilot-token-placeholder optional: true - - name: CODEX_ACCESS_TOKEN - valueFrom: - secretKeyRef: - name: netclode-secrets - key: codex-access-token-placeholder - optional: true - - name: CODEX_ID_TOKEN - valueFrom: - secretKeyRef: - name: netclode-secrets - key: codex-id-token-placeholder - optional: true - - name: CODEX_REFRESH_TOKEN - valueFrom: - secretKeyRef: - name: netclode-secrets - key: codex-refresh-token-placeholder - optional: true # GITHUB_TOKEN is NOT proxied - used for git credential helper (clone/push) # Git embeds credentials in URL, doesn't use HTTP Authorization headers - name: GITHUB_TOKEN diff --git a/proto/netclode/v1/agent.proto b/proto/netclode/v1/agent.proto index 770ce3c4..fbb9f5a3 100644 --- a/proto/netclode/v1/agent.proto +++ b/proto/netclode/v1/agent.proto @@ -4,6 +4,7 @@ package netclode.v1; import "netclode/v1/common.proto"; import "netclode/v1/events.proto"; +import "google/protobuf/timestamp.proto"; // AgentService handles bidirectional communication between agents and control plane. // Agents connect to the control plane (not the other way around). @@ -64,6 +65,9 @@ message ControlPlaneMessage { // Session assigned (warm pool mode) - pushed when claim binds SessionAssigned session_assigned = 9; + + // Update Codex OAuth tokens for an active session. + UpdateCodexAuth update_codex_auth = 10; } } @@ -202,4 +206,9 @@ message UpdateGitCredentials { RepoAccess repo_access = 2; // New access level (for logging) } - +// UpdateCodexAuth updates short-lived Codex OAuth tokens for the running agent. +message UpdateCodexAuth { + string access_token = 1; + string id_token = 2; + optional google.protobuf.Timestamp expires_at = 3; +} diff --git a/proto/netclode/v1/client.proto b/proto/netclode/v1/client.proto index cf507a10..b6672c5d 100644 --- a/proto/netclode/v1/client.proto +++ b/proto/netclode/v1/client.proto @@ -41,6 +41,11 @@ message ClientMessage { UpdateRepoAccessRequest update_repo_access = 21; // Resource limits GetResourceLimitsRequest get_resource_limits = 22; + // Backend-managed Codex OAuth flow + CodexAuthStartRequest codex_auth_start = 23; + CodexAuthStatusRequest codex_auth_status = 24; + CodexAuthLogoutRequest codex_auth_logout = 25; + UnexposePortRequest unexpose_port = 26; } } @@ -72,6 +77,11 @@ message ServerMessage { RepoAccessUpdatedResponse repo_access_updated = 23; // Resource limits ResourceLimitsResponse resource_limits = 24; + // Backend-managed Codex OAuth flow + CodexAuthStartedResponse codex_auth_started = 25; + CodexAuthStatusResponse codex_auth_status = 26; + CodexAuthLoggedOutResponse codex_auth_logged_out = 27; + PortUnexposedResponse port_unexposed = 28; } } @@ -87,6 +97,14 @@ message NetworkConfig { bool tailnet_access = 1; } +// CodexOAuthTokens contains ChatGPT OAuth tokens for Codex sessions. +message CodexOAuthTokens { + string access_token = 1; + string id_token = 2; + string refresh_token = 3; + optional google.protobuf.Timestamp expires_at = 4; +} + message CreateSessionRequest { optional string request_id = 1; // Client-generated ID for request correlation optional string name = 2; // Initial session name @@ -98,6 +116,7 @@ message CreateSessionRequest { optional CopilotBackend copilot_backend = 8; // Backend for Copilot SDK (GitHub or Anthropic) optional NetworkConfig network_config = 9; // Network configuration (defaults to enabled) optional SandboxResources resources = 10; // Custom VM resources (bypasses warm pool if set) + optional CodexOAuthTokens codex_oauth_tokens = 11; // Session-scoped OAuth tokens for Codex :oauth models } message ListSessionsRequest { @@ -160,6 +179,12 @@ message ExposePortRequest { int32 port = 3; } +message UnexposePortRequest { + optional string request_id = 1; + string session_id = 2; + int32 port = 3; +} + message SyncRequest { optional string request_id = 1; } @@ -183,6 +208,7 @@ message ListModelsRequest { optional string request_id = 1; SdkType sdk_type = 2; // Which SDK to list models for optional CopilotBackend copilot_backend = 3; // For Copilot: which backend's models to list + optional bool codex_oauth_available = 4; // Hint from client to include Codex :oauth model variants } message GetCopilotStatusRequest { @@ -210,6 +236,18 @@ message GetResourceLimitsRequest { optional string request_id = 1; } +message CodexAuthStartRequest { + optional string request_id = 1; +} + +message CodexAuthStatusRequest { + optional string request_id = 1; +} + +message CodexAuthLogoutRequest { + optional string request_id = 1; +} + // ============================================================================ // Server Response Messages // ============================================================================ @@ -267,6 +305,12 @@ message PortExposedResponse { optional string request_id = 4; } +message PortUnexposedResponse { + string session_id = 1; + int32 port = 2; + optional string request_id = 3; +} + message GitHubReposResponse { repeated GitHubRepo repos = 1; optional string request_id = 2; @@ -303,6 +347,35 @@ message CopilotStatusResponse { optional string request_id = 3; } +enum CodexAuthState { + CODEX_AUTH_STATE_UNSPECIFIED = 0; + CODEX_AUTH_STATE_UNAUTHENTICATED = 1; + CODEX_AUTH_STATE_PENDING = 2; + CODEX_AUTH_STATE_READY = 3; + CODEX_AUTH_STATE_ERROR = 4; +} + +message CodexAuthStartedResponse { + string verification_uri = 1; + optional string verification_uri_complete = 2; + string user_code = 3; + int32 interval_seconds = 4; + google.protobuf.Timestamp expires_at = 5; + optional string request_id = 6; +} + +message CodexAuthStatusResponse { + CodexAuthState state = 1; + optional string account_id = 2; + optional google.protobuf.Timestamp expires_at = 3; + optional string error = 4; + optional string request_id = 5; +} + +message CodexAuthLoggedOutResponse { + optional string request_id = 1; +} + // ============================================================================ // Snapshot Response Messages // ============================================================================ diff --git a/proto/netclode/v1/common.proto b/proto/netclode/v1/common.proto index 425c75ad..afae4763 100644 --- a/proto/netclode/v1/common.proto +++ b/proto/netclode/v1/common.proto @@ -97,7 +97,6 @@ message SessionConfig { optional string codex_access_token = 11; optional string codex_id_token = 12; optional string openai_api_key = 13; - optional string codex_refresh_token = 14; optional string reasoning_effort = 15; optional string mistral_api_key = 16; optional string ollama_url = 17; // URL for local Ollama inference (e.g., "http://ollama.netclode.svc.cluster.local:11434") diff --git a/proto/netclode/v1/events.proto b/proto/netclode/v1/events.proto index b0324054..4a1c3df6 100644 --- a/proto/netclode/v1/events.proto +++ b/proto/netclode/v1/events.proto @@ -23,6 +23,7 @@ enum AgentEventKind { AGENT_EVENT_KIND_REPO_CLONE = 8; // Repository clone progress AGENT_EVENT_KIND_AGENT_DISCONNECTED = 9; // Agent disconnected unexpectedly AGENT_EVENT_KIND_AGENT_RECONNECTED = 10; // Agent reconnected after disconnect + AGENT_EVENT_KIND_PORT_UNEXPOSED = 11; // Port exposure was removed } // AgentEvent represents events emitted during agent execution. @@ -40,6 +41,7 @@ message AgentEvent { ToolEndPayload tool_end = 8; PortExposedPayload port_exposed = 9; RepoClonePayload repo_clone = 10; + PortUnexposedPayload port_unexposed = 11; } } @@ -99,6 +101,11 @@ message PortExposedPayload { optional string preview_url = 3; // URL to access the exposed port } +// PortUnexposedPayload contains data for port removal events. +message PortUnexposedPayload { + int32 port = 1; // The port number no longer exposed +} + // RepoCloneStage represents the stage of repository cloning. enum RepoCloneStage { REPO_CLONE_STAGE_UNSPECIFIED = 0; diff --git a/scripts/dev/build-on-remote.sh b/scripts/dev/build-on-remote.sh new file mode 100755 index 00000000..931ca017 --- /dev/null +++ b/scripts/dev/build-on-remote.sh @@ -0,0 +1,107 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd) + +if [[ -f "$ROOT_DIR/.env" ]]; then + set -a + # shellcheck disable=SC1091 + source "$ROOT_DIR/.env" + set +a +fi + +DEPLOY_HOST=${DEPLOY_HOST:-} +if [[ -z "$DEPLOY_HOST" ]]; then + echo "DEPLOY_HOST is required (.env or env var)." >&2 + exit 1 +fi + +# SSH target defaults to DEPLOY_HOST so user/login comes from ~/.ssh/config. +DEPLOY_SSH_TARGET=${DEPLOY_SSH_TARGET:-$DEPLOY_HOST} +TAG=${TAG:-dev-$(date +%Y%m%d-%H%M%S)} +REMOTE_DIR=${REMOTE_DIR:-/tmp/netclode-dev-worktree} +BUILD_CONTROL_PLANE=${BUILD_CONTROL_PLANE:-1} +BUILD_AGENT=${BUILD_AGENT:-1} +IMPORT_TO_K3S=${IMPORT_TO_K3S:-1} + +CONTROL_PLANE_IMAGE=${CONTROL_PLANE_IMAGE:-netclode-control-plane:$TAG} +AGENT_IMAGE=${AGENT_IMAGE:-netclode-agent:$TAG} + +if [[ "$BUILD_CONTROL_PLANE" != "1" && "$BUILD_AGENT" != "1" ]]; then + echo "Nothing to build. Set BUILD_CONTROL_PLANE=1 and/or BUILD_AGENT=1." >&2 + exit 1 +fi + +for cmd in rsync ssh; do + if ! command -v "$cmd" >/dev/null 2>&1; then + echo "Missing required command: $cmd" >&2 + exit 1 + fi +done + +echo "[dev-build] Preparing remote workspace on $DEPLOY_SSH_TARGET:$REMOTE_DIR" +ssh "$DEPLOY_SSH_TARGET" "mkdir -p '$REMOTE_DIR/services'" + +if [[ "$BUILD_CONTROL_PLANE" == "1" ]]; then + echo "[dev-build] Syncing services/control-plane" + rsync -az --delete \ + --exclude '.DS_Store' \ + "$ROOT_DIR/services/control-plane/" "$DEPLOY_SSH_TARGET:$REMOTE_DIR/services/control-plane/" +fi + +if [[ "$BUILD_AGENT" == "1" ]]; then + echo "[dev-build] Syncing services/agent" + rsync -az --delete \ + --exclude '.DS_Store' \ + --exclude 'node_modules' \ + --exclude 'dist' \ + "$ROOT_DIR/services/agent/" "$DEPLOY_SSH_TARGET:$REMOTE_DIR/services/agent/" +fi + +echo "[dev-build] Building images on $DEPLOY_SSH_TARGET" +ssh "$DEPLOY_SSH_TARGET" \ + "TAG='$TAG' REMOTE_DIR='$REMOTE_DIR' BUILD_CONTROL_PLANE='$BUILD_CONTROL_PLANE' BUILD_AGENT='$BUILD_AGENT' IMPORT_TO_K3S='$IMPORT_TO_K3S' CONTROL_PLANE_IMAGE='$CONTROL_PLANE_IMAGE' AGENT_IMAGE='$AGENT_IMAGE' bash -se" <<'EOSSH' +set -euo pipefail + +cd "$REMOTE_DIR" + +if [[ "$BUILD_CONTROL_PLANE" == "1" || "$BUILD_AGENT" == "1" ]]; then + if ! command -v docker >/dev/null 2>&1; then + echo "[remote-build] docker is required on the remote host but was not found in PATH." >&2 + echo "[remote-build] Install Docker (with buildx) on the dev host or disable remote build targets." >&2 + exit 127 + fi +fi + +if [[ "$BUILD_CONTROL_PLANE" == "1" ]]; then + echo "[remote-build] Building control-plane image: $CONTROL_PLANE_IMAGE" + docker buildx build --platform linux/amd64 \ + -f services/control-plane/Dockerfile \ + -t "$CONTROL_PLANE_IMAGE" \ + --load services/control-plane + if [[ "$IMPORT_TO_K3S" == "1" ]]; then + echo "[remote-build] Importing control-plane image into k3s containerd" + docker save "$CONTROL_PLANE_IMAGE" | sudo k3s ctr images import - + fi +fi + +if [[ "$BUILD_AGENT" == "1" ]]; then + echo "[remote-build] Building agent image: $AGENT_IMAGE" + docker buildx build --platform linux/amd64 \ + -f services/agent/Dockerfile \ + -t "$AGENT_IMAGE" \ + --load . + if [[ "$IMPORT_TO_K3S" == "1" ]]; then + echo "[remote-build] Importing agent image into k3s containerd" + docker save "$AGENT_IMAGE" | sudo k3s ctr images import - + fi +fi +EOSSH + +echo "[dev-build] Built images" +if [[ "$BUILD_CONTROL_PLANE" == "1" ]]; then + echo " CONTROL_PLANE_IMAGE=$CONTROL_PLANE_IMAGE" +fi +if [[ "$BUILD_AGENT" == "1" ]]; then + echo " AGENT_IMAGE=$AGENT_IMAGE" +fi diff --git a/scripts/dev/deploy-dev-images.sh b/scripts/dev/deploy-dev-images.sh new file mode 100755 index 00000000..c856a2a2 --- /dev/null +++ b/scripts/dev/deploy-dev-images.sh @@ -0,0 +1,54 @@ +#!/usr/bin/env bash +set -euo pipefail + +CONTEXT=${CONTEXT:-netclode} +NAMESPACE=${NAMESPACE:-netclode} +WARM_POOL_NAME=${WARM_POOL_NAME:-netclode-agent-pool} +SANDBOX_TEMPLATE=${SANDBOX_TEMPLATE:-netclode-agent} +CONTROL_PLANE_DEPLOYMENT=${CONTROL_PLANE_DEPLOYMENT:-control-plane} +REFRESH_WARM_POOL=${REFRESH_WARM_POOL:-1} + +CONTROL_PLANE_IMAGE=${CONTROL_PLANE_IMAGE:-} +AGENT_IMAGE=${AGENT_IMAGE:-} + +if [[ -z "$CONTROL_PLANE_IMAGE" || -z "$AGENT_IMAGE" ]]; then + echo "CONTROL_PLANE_IMAGE and AGENT_IMAGE are required." >&2 + echo "Example: CONTROL_PLANE_IMAGE=netclode-control-plane:dev-123 AGENT_IMAGE=netclode-agent:dev-123 $0" >&2 + exit 1 +fi + +KUBECTL=(kubectl --context "$CONTEXT" -n "$NAMESPACE") + +echo "[dev-deploy] Updating control-plane deployment image to $CONTROL_PLANE_IMAGE" +"${KUBECTL[@]}" set image "deployment/$CONTROL_PLANE_DEPLOYMENT" "control-plane=$CONTROL_PLANE_IMAGE" + +echo "[dev-deploy] Ensuring control-plane uses AGENT_IMAGE=$AGENT_IMAGE" +"${KUBECTL[@]}" set env "deployment/$CONTROL_PLANE_DEPLOYMENT" "AGENT_IMAGE=$AGENT_IMAGE" + +echo "[dev-deploy] Setting control-plane imagePullPolicy=IfNotPresent for local dev images" +"${KUBECTL[@]}" patch "deployment/$CONTROL_PLANE_DEPLOYMENT" --type='json' -p="[ + {\"op\":\"replace\",\"path\":\"/spec/template/spec/containers/0/imagePullPolicy\",\"value\":\"IfNotPresent\"} +]" + +echo "[dev-deploy] Updating sandbox template image to $AGENT_IMAGE" +"${KUBECTL[@]}" patch "sandboxtemplate/$SANDBOX_TEMPLATE" --type='json' -p="[ + {\"op\":\"replace\",\"path\":\"/spec/podTemplate/spec/containers/0/image\",\"value\":\"$AGENT_IMAGE\"}, + {\"op\":\"replace\",\"path\":\"/spec/podTemplate/spec/containers/0/imagePullPolicy\",\"value\":\"IfNotPresent\"} +]" + +echo "[dev-deploy] Waiting for control-plane rollout" +"${KUBECTL[@]}" rollout status "deployment/$CONTROL_PLANE_DEPLOYMENT" --timeout=180s + +if [[ "$REFRESH_WARM_POOL" == "1" ]]; then + current_replicas=$("${KUBECTL[@]}" get "sandboxwarmpool/$WARM_POOL_NAME" -o jsonpath='{.spec.replicas}') + if [[ -z "$current_replicas" ]]; then + current_replicas=1 + fi + + echo "[dev-deploy] Refreshing warm pool ($WARM_POOL_NAME): $current_replicas -> 0 -> $current_replicas" + "${KUBECTL[@]}" patch "sandboxwarmpool/$WARM_POOL_NAME" --type=merge -p '{"spec":{"replicas":0}}' + sleep 3 + "${KUBECTL[@]}" patch "sandboxwarmpool/$WARM_POOL_NAME" --type=merge -p "{\"spec\":{\"replicas\":$current_replicas}}" +fi + +echo "[dev-deploy] Done" diff --git a/scripts/dev/verify-dev-loop.sh b/scripts/dev/verify-dev-loop.sh new file mode 100755 index 00000000..14417baf --- /dev/null +++ b/scripts/dev/verify-dev-loop.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash +set -euo pipefail + +CONTEXT=${CONTEXT:-netclode} +NAMESPACE=${NAMESPACE:-netclode} +CONTROL_PLANE_DEPLOYMENT=${CONTROL_PLANE_DEPLOYMENT:-control-plane} +SANDBOX_TEMPLATE=${SANDBOX_TEMPLATE:-netclode-agent} + +KUBECTL=(kubectl --context "$CONTEXT" -n "$NAMESPACE") + +echo "[verify] control-plane rollout" +"${KUBECTL[@]}" rollout status "deployment/$CONTROL_PLANE_DEPLOYMENT" --timeout=180s + +echo "[verify] control-plane image" +"${KUBECTL[@]}" get "deployment/$CONTROL_PLANE_DEPLOYMENT" -o jsonpath='{.spec.template.spec.containers[0].image}{"\n"}' + +echo "[verify] control-plane AGENT_IMAGE env" +"${KUBECTL[@]}" get "deployment/$CONTROL_PLANE_DEPLOYMENT" -o jsonpath='{.spec.template.spec.containers[0].env[?(@.name=="AGENT_IMAGE")].value}{"\n"}' + +echo "[verify] sandbox template agent image" +"${KUBECTL[@]}" get "sandboxtemplate/$SANDBOX_TEMPLATE" -o jsonpath='{.spec.podTemplate.spec.containers[0].image}{"\n"}' + +echo "[verify] sandbox template imagePullPolicy" +"${KUBECTL[@]}" get "sandboxtemplate/$SANDBOX_TEMPLATE" -o jsonpath='{.spec.podTemplate.spec.containers[0].imagePullPolicy}{"\n"}' + +echo "[verify] recent control-plane logs" +"${KUBECTL[@]}" logs "deployment/$CONTROL_PLANE_DEPLOYMENT" --tail=60 diff --git a/services/agent/Dockerfile b/services/agent/Dockerfile index 3ec3a95b..c1c3b2dc 100644 --- a/services/agent/Dockerfile +++ b/services/agent/Dockerfile @@ -53,12 +53,17 @@ WORKDIR /build # Install esbuild for bundling RUN npm install -g esbuild -# Copy agent source -COPY services/agent services/agent +# Copy dependency manifests first for better layer caching +COPY services/agent/package.json services/agent/package-lock.json services/agent/tsconfig.json /build/services/agent/ -# Install agent dependencies +# Install agent dependencies (only invalidates when manifest changes) WORKDIR /build/services/agent RUN --mount=type=cache,target=/root/.npm npm install +RUN npm rebuild node-pty --build-from-source + +# Copy source after dependency install to avoid reinstall on every TS edit +COPY services/agent/src /build/services/agent/src +COPY services/agent/gen /build/services/agent/gen # Bundle agent RUN esbuild src/index.ts --bundle --platform=node --format=esm --packages=external --outfile=dist/agent.js @@ -158,7 +163,8 @@ RUN apt-get update && apt-get install -y --no-install-recommends unzip && \ # # OpenCode checks if node_modules exists in its config dir before running bun add. # By pre-creating this directory with the plugin installed, we skip the ~4s install. -# Note: @opencode-ai/plugin@1.1.45 has broken workspace references, so we use 1.1.44 as fallback +# Keep a fixed fallback in case the latest OpenCode binary reports a plugin version +# that is not yet published on npm. RUN OPENCODE_VERSION=$(/usr/local/bin/opencode --version) && \ export HOME=/opt/bun-cache-home && \ mkdir -p $HOME/.bun && \ @@ -167,8 +173,8 @@ RUN OPENCODE_VERSION=$(/usr/local/bin/opencode --version) && \ if /opt/bun add "@opencode-ai/plugin@${OPENCODE_VERSION}" "@opencode-ai/sdk@${OPENCODE_VERSION}" "zod" --exact; then \ echo "Preinstalled OpenCode plugin ${OPENCODE_VERSION}"; \ else \ - echo "Warning: OpenCode plugin ${OPENCODE_VERSION} not found, falling back to 1.1.44"; \ - /opt/bun add "@opencode-ai/plugin@1.1.44" "@opencode-ai/sdk@1.1.44" "zod@4.1.8" --exact; \ + echo "Warning: OpenCode plugin ${OPENCODE_VERSION} not found, falling back to 1.1.53"; \ + /opt/bun add "@opencode-ai/plugin@1.1.53" "@opencode-ai/sdk@1.1.53" "zod@4.3.6" --exact; \ fi && \ if [ -d "$HOME/.bun" ]; then mv $HOME/.bun /opt/bun-cache; fi && \ rm -rf $HOME diff --git a/services/agent/gen/netclode/v1/agent_pb.ts b/services/agent/gen/netclode/v1/agent_pb.ts index e4b126c8..9c30f4fe 100644 --- a/services/agent/gen/netclode/v1/agent_pb.ts +++ b/services/agent/gen/netclode/v1/agent_pb.ts @@ -8,13 +8,15 @@ import type { GitFileChange, RepoAccess, SessionConfig } from "./common_pb"; import { file_netclode_v1_common } from "./common_pb"; import type { AgentEvent } from "./events_pb"; import { file_netclode_v1_events } from "./events_pb"; +import type { Timestamp } from "@bufbuild/protobuf/wkt"; +import { file_google_protobuf_timestamp } from "@bufbuild/protobuf/wkt"; import type { Message } from "@bufbuild/protobuf"; /** * Describes the file netclode/v1/agent.proto. */ export const file_netclode_v1_agent: GenFile = /*@__PURE__*/ - fileDesc("ChduZXRjbG9kZS92MS9hZ2VudC5wcm90bxILbmV0Y2xvZGUudjEiggMKDEFnZW50TWVzc2FnZRIuCghyZWdpc3RlchgBIAEoCzIaLm5ldGNsb2RlLnYxLkFnZW50UmVnaXN0ZXJIABI7Cg9wcm9tcHRfcmVzcG9uc2UYAiABKAsyIC5uZXRjbG9kZS52MS5BZ2VudFN0cmVhbVJlc3BvbnNlSAASOwoPdGVybWluYWxfb3V0cHV0GAMgASgLMiAubmV0Y2xvZGUudjEuQWdlbnRUZXJtaW5hbE91dHB1dEgAEjkKDnRpdGxlX3Jlc3BvbnNlGAQgASgLMh8ubmV0Y2xvZGUudjEuQWdlbnRUaXRsZVJlc3BvbnNlSAASQgoTZ2l0X3N0YXR1c19yZXNwb25zZRgFIAEoCzIjLm5ldGNsb2RlLnYxLkFnZW50R2l0U3RhdHVzUmVzcG9uc2VIABI+ChFnaXRfZGlmZl9yZXNwb25zZRgGIAEoCzIhLm5ldGNsb2RlLnYxLkFnZW50R2l0RGlmZlJlc3BvbnNlSABCCQoHbWVzc2FnZSKwBAoTQ29udHJvbFBsYW5lTWVzc2FnZRIyCgpyZWdpc3RlcmVkGAEgASgLMhwubmV0Y2xvZGUudjEuQWdlbnRSZWdpc3RlcmVkSAASOwoOZXhlY3V0ZV9wcm9tcHQYAiABKAsyIS5uZXRjbG9kZS52MS5FeGVjdXRlUHJvbXB0UmVxdWVzdEgAEjIKCWludGVycnVwdBgDIAEoCzIdLm5ldGNsb2RlLnYxLkludGVycnVwdFJlcXVlc3RIABI7Cg5nZW5lcmF0ZV90aXRsZRgEIAEoCzIhLm5ldGNsb2RlLnYxLkdlbmVyYXRlVGl0bGVSZXF1ZXN0SAASOgoOZ2V0X2dpdF9zdGF0dXMYBSABKAsyIC5uZXRjbG9kZS52MS5HZXRHaXRTdGF0dXNSZXF1ZXN0SAASNgoMZ2V0X2dpdF9kaWZmGAYgASgLMh4ubmV0Y2xvZGUudjEuR2V0R2l0RGlmZlJlcXVlc3RIABI5Cg50ZXJtaW5hbF9pbnB1dBgHIAEoCzIfLm5ldGNsb2RlLnYxLkFnZW50VGVybWluYWxJbnB1dEgAEkMKFnVwZGF0ZV9naXRfY3JlZGVudGlhbHMYCCABKAsyIS5uZXRjbG9kZS52MS5VcGRhdGVHaXRDcmVkZW50aWFsc0gAEjgKEHNlc3Npb25fYXNzaWduZWQYCSABKAsyHC5uZXRjbG9kZS52MS5TZXNzaW9uQXNzaWduZWRIAEIJCgdtZXNzYWdlIlEKD1Nlc3Npb25Bc3NpZ25lZBISCgpzZXNzaW9uX2lkGAEgASgJEioKBmNvbmZpZxgCIAEoCzIaLm5ldGNsb2RlLnYxLlNlc3Npb25Db25maWcikgEKDUFnZW50UmVnaXN0ZXISFwoKc2Vzc2lvbl9pZBgBIAEoCUgAiAEBEg8KB3ZlcnNpb24YAiABKAkSFQoIcG9kX25hbWUYAyABKAlIAYgBARIWCglrOHNfdG9rZW4YBCABKAlIAogBAUINCgtfc2Vzc2lvbl9pZEILCglfcG9kX25hbWVCDAoKX2s4c190b2tlbiKPAgoTQWdlbnRTdHJlYW1SZXNwb25zZRIxCgp0ZXh0X2RlbHRhGAEgASgLMhsubmV0Y2xvZGUudjEuQWdlbnRUZXh0RGVsdGFIABIoCgVldmVudBgCIAEoCzIXLm5ldGNsb2RlLnYxLkFnZW50RXZlbnRIABI5Cg5zeXN0ZW1fbWVzc2FnZRgDIAEoCzIfLm5ldGNsb2RlLnYxLkFnZW50U3lzdGVtTWVzc2FnZUgAEioKBnJlc3VsdBgEIAEoCzIYLm5ldGNsb2RlLnYxLkFnZW50UmVzdWx0SAASKAoFZXJyb3IYBSABKAsyFy5uZXRjbG9kZS52MS5BZ2VudEVycm9ySABCCgoIcmVzcG9uc2UiRgoOQWdlbnRUZXh0RGVsdGESDwoHY29udGVudBgBIAEoCRIPCgdwYXJ0aWFsGAIgASgIEhIKCm1lc3NhZ2VfaWQYAyABKAkiJQoSQWdlbnRTeXN0ZW1NZXNzYWdlEg8KB21lc3NhZ2UYASABKAkiTwoLQWdlbnRSZXN1bHQSFAoMaW5wdXRfdG9rZW5zGAEgASgFEhUKDW91dHB1dF90b2tlbnMYAiABKAUSEwoLdG90YWxfdHVybnMYAyABKAUiMAoKQWdlbnRFcnJvchIPCgdtZXNzYWdlGAEgASgJEhEKCXJldHJ5YWJsZRgCIAEoCCIjChNBZ2VudFRlcm1pbmFsT3V0cHV0EgwKBGRhdGEYASABKAkiNwoSQWdlbnRUaXRsZVJlc3BvbnNlEhIKCnJlcXVlc3RfaWQYASABKAkSDQoFdGl0bGUYAiABKAkiVwoWQWdlbnRHaXRTdGF0dXNSZXNwb25zZRISCgpyZXF1ZXN0X2lkGAEgASgJEikKBWZpbGVzGAIgAygLMhoubmV0Y2xvZGUudjEuR2l0RmlsZUNoYW5nZSI4ChRBZ2VudEdpdERpZmZSZXNwb25zZRISCgpyZXF1ZXN0X2lkGAEgASgJEgwKBGRpZmYYAiABKAkifAoPQWdlbnRSZWdpc3RlcmVkEg8KB3N1Y2Nlc3MYASABKAgSEgoFZXJyb3IYAiABKAlIAIgBARIvCgZjb25maWcYAyABKAsyGi5uZXRjbG9kZS52MS5TZXNzaW9uQ29uZmlnSAGIAQFCCAoGX2Vycm9yQgkKB19jb25maWciJAoURXhlY3V0ZVByb21wdFJlcXVlc3QSDAoEdGV4dBgBIAEoCSISChBJbnRlcnJ1cHRSZXF1ZXN0IjoKFEdlbmVyYXRlVGl0bGVSZXF1ZXN0EhIKCnJlcXVlc3RfaWQYASABKAkSDgoGcHJvbXB0GAIgASgJIikKE0dldEdpdFN0YXR1c1JlcXVlc3QSEgoKcmVxdWVzdF9pZBgBIAEoCSJDChFHZXRHaXREaWZmUmVxdWVzdBISCgpyZXF1ZXN0X2lkGAEgASgJEhEKBGZpbGUYAiABKAlIAIgBAUIHCgVfZmlsZSJhChJBZ2VudFRlcm1pbmFsSW5wdXQSDgoEZGF0YRgBIAEoCUgAEjIKBnJlc2l6ZRgCIAEoCzIgLm5ldGNsb2RlLnYxLkFnZW50VGVybWluYWxSZXNpemVIAEIHCgVpbnB1dCIxChNBZ2VudFRlcm1pbmFsUmVzaXplEgwKBGNvbHMYASABKAUSDAoEcm93cxgCIAEoBSJaChRVcGRhdGVHaXRDcmVkZW50aWFscxIUCgxnaXRodWJfdG9rZW4YASABKAkSLAoLcmVwb19hY2Nlc3MYAiABKA4yFy5uZXRjbG9kZS52MS5SZXBvQWNjZXNzMloKDEFnZW50U2VydmljZRJKCgdDb25uZWN0EhkubmV0Y2xvZGUudjEuQWdlbnRNZXNzYWdlGiAubmV0Y2xvZGUudjEuQ29udHJvbFBsYW5lTWVzc2FnZSgBMAFCuwEKD2NvbS5uZXRjbG9kZS52MUIKQWdlbnRQcm90b1ABWk9naXRodWIuY29tL2FuZ3Jpc3Rhbi9uZXRjbG9kZS9zZXJ2aWNlcy9jb250cm9sLXBsYW5lL2dlbi9uZXRjbG9kZS92MTtuZXRjbG9kZXYxogIDTlhYqgILTmV0Y2xvZGUuVjHKAgtOZXRjbG9kZVxWMeICF05ldGNsb2RlXFYxXEdQQk1ldGFkYXRh6gIMTmV0Y2xvZGU6OlYxYgZwcm90bzM", [file_netclode_v1_common, file_netclode_v1_events]); + fileDesc("ChduZXRjbG9kZS92MS9hZ2VudC5wcm90bxILbmV0Y2xvZGUudjEiggMKDEFnZW50TWVzc2FnZRIuCghyZWdpc3RlchgBIAEoCzIaLm5ldGNsb2RlLnYxLkFnZW50UmVnaXN0ZXJIABI7Cg9wcm9tcHRfcmVzcG9uc2UYAiABKAsyIC5uZXRjbG9kZS52MS5BZ2VudFN0cmVhbVJlc3BvbnNlSAASOwoPdGVybWluYWxfb3V0cHV0GAMgASgLMiAubmV0Y2xvZGUudjEuQWdlbnRUZXJtaW5hbE91dHB1dEgAEjkKDnRpdGxlX3Jlc3BvbnNlGAQgASgLMh8ubmV0Y2xvZGUudjEuQWdlbnRUaXRsZVJlc3BvbnNlSAASQgoTZ2l0X3N0YXR1c19yZXNwb25zZRgFIAEoCzIjLm5ldGNsb2RlLnYxLkFnZW50R2l0U3RhdHVzUmVzcG9uc2VIABI+ChFnaXRfZGlmZl9yZXNwb25zZRgGIAEoCzIhLm5ldGNsb2RlLnYxLkFnZW50R2l0RGlmZlJlc3BvbnNlSABCCQoHbWVzc2FnZSLrBAoTQ29udHJvbFBsYW5lTWVzc2FnZRIyCgpyZWdpc3RlcmVkGAEgASgLMhwubmV0Y2xvZGUudjEuQWdlbnRSZWdpc3RlcmVkSAASOwoOZXhlY3V0ZV9wcm9tcHQYAiABKAsyIS5uZXRjbG9kZS52MS5FeGVjdXRlUHJvbXB0UmVxdWVzdEgAEjIKCWludGVycnVwdBgDIAEoCzIdLm5ldGNsb2RlLnYxLkludGVycnVwdFJlcXVlc3RIABI7Cg5nZW5lcmF0ZV90aXRsZRgEIAEoCzIhLm5ldGNsb2RlLnYxLkdlbmVyYXRlVGl0bGVSZXF1ZXN0SAASOgoOZ2V0X2dpdF9zdGF0dXMYBSABKAsyIC5uZXRjbG9kZS52MS5HZXRHaXRTdGF0dXNSZXF1ZXN0SAASNgoMZ2V0X2dpdF9kaWZmGAYgASgLMh4ubmV0Y2xvZGUudjEuR2V0R2l0RGlmZlJlcXVlc3RIABI5Cg50ZXJtaW5hbF9pbnB1dBgHIAEoCzIfLm5ldGNsb2RlLnYxLkFnZW50VGVybWluYWxJbnB1dEgAEkMKFnVwZGF0ZV9naXRfY3JlZGVudGlhbHMYCCABKAsyIS5uZXRjbG9kZS52MS5VcGRhdGVHaXRDcmVkZW50aWFsc0gAEjgKEHNlc3Npb25fYXNzaWduZWQYCSABKAsyHC5uZXRjbG9kZS52MS5TZXNzaW9uQXNzaWduZWRIABI5ChF1cGRhdGVfY29kZXhfYXV0aBgKIAEoCzIcLm5ldGNsb2RlLnYxLlVwZGF0ZUNvZGV4QXV0aEgAQgkKB21lc3NhZ2UiUQoPU2Vzc2lvbkFzc2lnbmVkEhIKCnNlc3Npb25faWQYASABKAkSKgoGY29uZmlnGAIgASgLMhoubmV0Y2xvZGUudjEuU2Vzc2lvbkNvbmZpZyKSAQoNQWdlbnRSZWdpc3RlchIXCgpzZXNzaW9uX2lkGAEgASgJSACIAQESDwoHdmVyc2lvbhgCIAEoCRIVCghwb2RfbmFtZRgDIAEoCUgBiAEBEhYKCWs4c190b2tlbhgEIAEoCUgCiAEBQg0KC19zZXNzaW9uX2lkQgsKCV9wb2RfbmFtZUIMCgpfazhzX3Rva2VuIo8CChNBZ2VudFN0cmVhbVJlc3BvbnNlEjEKCnRleHRfZGVsdGEYASABKAsyGy5uZXRjbG9kZS52MS5BZ2VudFRleHREZWx0YUgAEigKBWV2ZW50GAIgASgLMhcubmV0Y2xvZGUudjEuQWdlbnRFdmVudEgAEjkKDnN5c3RlbV9tZXNzYWdlGAMgASgLMh8ubmV0Y2xvZGUudjEuQWdlbnRTeXN0ZW1NZXNzYWdlSAASKgoGcmVzdWx0GAQgASgLMhgubmV0Y2xvZGUudjEuQWdlbnRSZXN1bHRIABIoCgVlcnJvchgFIAEoCzIXLm5ldGNsb2RlLnYxLkFnZW50RXJyb3JIAEIKCghyZXNwb25zZSJGCg5BZ2VudFRleHREZWx0YRIPCgdjb250ZW50GAEgASgJEg8KB3BhcnRpYWwYAiABKAgSEgoKbWVzc2FnZV9pZBgDIAEoCSIlChJBZ2VudFN5c3RlbU1lc3NhZ2USDwoHbWVzc2FnZRgBIAEoCSJPCgtBZ2VudFJlc3VsdBIUCgxpbnB1dF90b2tlbnMYASABKAUSFQoNb3V0cHV0X3Rva2VucxgCIAEoBRITCgt0b3RhbF90dXJucxgDIAEoBSIwCgpBZ2VudEVycm9yEg8KB21lc3NhZ2UYASABKAkSEQoJcmV0cnlhYmxlGAIgASgIIiMKE0FnZW50VGVybWluYWxPdXRwdXQSDAoEZGF0YRgBIAEoCSI3ChJBZ2VudFRpdGxlUmVzcG9uc2USEgoKcmVxdWVzdF9pZBgBIAEoCRINCgV0aXRsZRgCIAEoCSJXChZBZ2VudEdpdFN0YXR1c1Jlc3BvbnNlEhIKCnJlcXVlc3RfaWQYASABKAkSKQoFZmlsZXMYAiADKAsyGi5uZXRjbG9kZS52MS5HaXRGaWxlQ2hhbmdlIjgKFEFnZW50R2l0RGlmZlJlc3BvbnNlEhIKCnJlcXVlc3RfaWQYASABKAkSDAoEZGlmZhgCIAEoCSJ8Cg9BZ2VudFJlZ2lzdGVyZWQSDwoHc3VjY2VzcxgBIAEoCBISCgVlcnJvchgCIAEoCUgAiAEBEi8KBmNvbmZpZxgDIAEoCzIaLm5ldGNsb2RlLnYxLlNlc3Npb25Db25maWdIAYgBAUIICgZfZXJyb3JCCQoHX2NvbmZpZyIkChRFeGVjdXRlUHJvbXB0UmVxdWVzdBIMCgR0ZXh0GAEgASgJIhIKEEludGVycnVwdFJlcXVlc3QiOgoUR2VuZXJhdGVUaXRsZVJlcXVlc3QSEgoKcmVxdWVzdF9pZBgBIAEoCRIOCgZwcm9tcHQYAiABKAkiKQoTR2V0R2l0U3RhdHVzUmVxdWVzdBISCgpyZXF1ZXN0X2lkGAEgASgJIkMKEUdldEdpdERpZmZSZXF1ZXN0EhIKCnJlcXVlc3RfaWQYASABKAkSEQoEZmlsZRgCIAEoCUgAiAEBQgcKBV9maWxlImEKEkFnZW50VGVybWluYWxJbnB1dBIOCgRkYXRhGAEgASgJSAASMgoGcmVzaXplGAIgASgLMiAubmV0Y2xvZGUudjEuQWdlbnRUZXJtaW5hbFJlc2l6ZUgAQgcKBWlucHV0IjEKE0FnZW50VGVybWluYWxSZXNpemUSDAoEY29scxgBIAEoBRIMCgRyb3dzGAIgASgFIloKFFVwZGF0ZUdpdENyZWRlbnRpYWxzEhQKDGdpdGh1Yl90b2tlbhgBIAEoCRIsCgtyZXBvX2FjY2VzcxgCIAEoDjIXLm5ldGNsb2RlLnYxLlJlcG9BY2Nlc3MifQoPVXBkYXRlQ29kZXhBdXRoEhQKDGFjY2Vzc190b2tlbhgBIAEoCRIQCghpZF90b2tlbhgCIAEoCRIzCgpleHBpcmVzX2F0GAMgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcEgAiAEBQg0KC19leHBpcmVzX2F0MloKDEFnZW50U2VydmljZRJKCgdDb25uZWN0EhkubmV0Y2xvZGUudjEuQWdlbnRNZXNzYWdlGiAubmV0Y2xvZGUudjEuQ29udHJvbFBsYW5lTWVzc2FnZSgBMAFCuwEKD2NvbS5uZXRjbG9kZS52MUIKQWdlbnRQcm90b1ABWk9naXRodWIuY29tL2FuZ3Jpc3Rhbi9uZXRjbG9kZS9zZXJ2aWNlcy9jb250cm9sLXBsYW5lL2dlbi9uZXRjbG9kZS92MTtuZXRjbG9kZXYxogIDTlhYqgILTmV0Y2xvZGUuVjHKAgtOZXRjbG9kZVxWMeICF05ldGNsb2RlXFYxXEdQQk1ldGFkYXRh6gIMTmV0Y2xvZGU6OlYxYgZwcm90bzM", [file_netclode_v1_common, file_netclode_v1_events, file_google_protobuf_timestamp]); /** * AgentMessage is sent from agent to control plane. @@ -164,6 +166,14 @@ export type ControlPlaneMessage = Message<"netclode.v1.ControlPlaneMessage"> & { */ value: SessionAssigned; case: "sessionAssigned"; + } | { + /** + * Update Codex OAuth tokens for an active session. + * + * @generated from field: netclode.v1.UpdateCodexAuth update_codex_auth = 10; + */ + value: UpdateCodexAuth; + case: "updateCodexAuth"; } | { case: undefined; value?: undefined }; }; @@ -768,6 +778,35 @@ export type UpdateGitCredentials = Message<"netclode.v1.UpdateGitCredentials"> & export const UpdateGitCredentialsSchema: GenMessage = /*@__PURE__*/ messageDesc(file_netclode_v1_agent, 21); +/** + * UpdateCodexAuth updates short-lived Codex OAuth tokens for the running agent. + * + * @generated from message netclode.v1.UpdateCodexAuth + */ +export type UpdateCodexAuth = Message<"netclode.v1.UpdateCodexAuth"> & { + /** + * @generated from field: string access_token = 1; + */ + accessToken: string; + + /** + * @generated from field: string id_token = 2; + */ + idToken: string; + + /** + * @generated from field: optional google.protobuf.Timestamp expires_at = 3; + */ + expiresAt?: Timestamp; +}; + +/** + * Describes the message netclode.v1.UpdateCodexAuth. + * Use `create(UpdateCodexAuthSchema)` to create a new message. + */ +export const UpdateCodexAuthSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_netclode_v1_agent, 22); + /** * AgentService handles bidirectional communication between agents and control plane. * Agents connect to the control plane (not the other way around). diff --git a/services/agent/gen/netclode/v1/client_pb.ts b/services/agent/gen/netclode/v1/client_pb.ts index 79be9271..14ad07f3 100644 --- a/services/agent/gen/netclode/v1/client_pb.ts +++ b/services/agent/gen/netclode/v1/client_pb.ts @@ -2,8 +2,8 @@ // @generated from file netclode/v1/client.proto (package netclode.v1, syntax proto3) /* eslint-disable */ -import type { GenFile, GenMessage, GenService } from "@bufbuild/protobuf/codegenv1"; -import { fileDesc, messageDesc, serviceDesc } from "@bufbuild/protobuf/codegenv1"; +import type { GenEnum, GenFile, GenMessage, GenService } from "@bufbuild/protobuf/codegenv1"; +import { enumDesc, fileDesc, messageDesc, serviceDesc } from "@bufbuild/protobuf/codegenv1"; import type { CopilotAuthStatus, CopilotBackend, CopilotPremiumQuota, Error, GitFileChange, GitHubRepo, InProgressState, ModelInfo, RepoAccess, SandboxResources, SdkType, Session, SessionSummary, Snapshot, StreamEntry } from "./common_pb"; import { file_netclode_v1_common } from "./common_pb"; import { file_netclode_v1_events } from "./events_pb"; @@ -15,7 +15,7 @@ import type { Message } from "@bufbuild/protobuf"; * Describes the file netclode/v1/client.proto. */ export const file_netclode_v1_client: GenFile = /*@__PURE__*/ - fileDesc("ChhuZXRjbG9kZS92MS9jbGllbnQucHJvdG8SC25ldGNsb2RlLnYxIsYKCg1DbGllbnRNZXNzYWdlEjsKDmNyZWF0ZV9zZXNzaW9uGAEgASgLMiEubmV0Y2xvZGUudjEuQ3JlYXRlU2Vzc2lvblJlcXVlc3RIABI5Cg1saXN0X3Nlc3Npb25zGAIgASgLMiAubmV0Y2xvZGUudjEuTGlzdFNlc3Npb25zUmVxdWVzdEgAEjcKDG9wZW5fc2Vzc2lvbhgDIAEoCzIfLm5ldGNsb2RlLnYxLk9wZW5TZXNzaW9uUmVxdWVzdEgAEjsKDnJlc3VtZV9zZXNzaW9uGAQgASgLMiEubmV0Y2xvZGUudjEuUmVzdW1lU2Vzc2lvblJlcXVlc3RIABI5Cg1wYXVzZV9zZXNzaW9uGAUgASgLMiAubmV0Y2xvZGUudjEuUGF1c2VTZXNzaW9uUmVxdWVzdEgAEjsKDmRlbGV0ZV9zZXNzaW9uGAYgASgLMiEubmV0Y2xvZGUudjEuRGVsZXRlU2Vzc2lvblJlcXVlc3RIABJEChNkZWxldGVfYWxsX3Nlc3Npb25zGAcgASgLMiUubmV0Y2xvZGUudjEuRGVsZXRlQWxsU2Vzc2lvbnNSZXF1ZXN0SAASNQoLc2VuZF9wcm9tcHQYCCABKAsyHi5uZXRjbG9kZS52MS5TZW5kUHJvbXB0UmVxdWVzdEgAEj8KEGludGVycnVwdF9wcm9tcHQYCSABKAsyIy5uZXRjbG9kZS52MS5JbnRlcnJ1cHRQcm9tcHRSZXF1ZXN0SAASOwoOdGVybWluYWxfaW5wdXQYCiABKAsyIS5uZXRjbG9kZS52MS5UZXJtaW5hbElucHV0UmVxdWVzdEgAEj0KD3Rlcm1pbmFsX3Jlc2l6ZRgLIAEoCzIiLm5ldGNsb2RlLnYxLlRlcm1pbmFsUmVzaXplUmVxdWVzdEgAEjUKC2V4cG9zZV9wb3J0GAwgASgLMh4ubmV0Y2xvZGUudjEuRXhwb3NlUG9ydFJlcXVlc3RIABIoCgRzeW5jGA0gASgLMhgubmV0Y2xvZGUudjEuU3luY1JlcXVlc3RIABJAChFsaXN0X2dpdGh1Yl9yZXBvcxgOIAEoCzIjLm5ldGNsb2RlLnYxLkxpc3RHaXRIdWJSZXBvc1JlcXVlc3RIABIzCgpnaXRfc3RhdHVzGA8gASgLMh0ubmV0Y2xvZGUudjEuR2l0U3RhdHVzUmVxdWVzdEgAEi8KCGdpdF9kaWZmGBAgASgLMhsubmV0Y2xvZGUudjEuR2l0RGlmZlJlcXVlc3RIABI1CgtsaXN0X21vZGVscxgRIAEoCzIeLm5ldGNsb2RlLnYxLkxpc3RNb2RlbHNSZXF1ZXN0SAASQgoSZ2V0X2NvcGlsb3Rfc3RhdHVzGBIgASgLMiQubmV0Y2xvZGUudjEuR2V0Q29waWxvdFN0YXR1c1JlcXVlc3RIABI7Cg5saXN0X3NuYXBzaG90cxgTIAEoCzIhLm5ldGNsb2RlLnYxLkxpc3RTbmFwc2hvdHNSZXF1ZXN0SAASPwoQcmVzdG9yZV9zbmFwc2hvdBgUIAEoCzIjLm5ldGNsb2RlLnYxLlJlc3RvcmVTbmFwc2hvdFJlcXVlc3RIABJCChJ1cGRhdGVfcmVwb19hY2Nlc3MYFSABKAsyJC5uZXRjbG9kZS52MS5VcGRhdGVSZXBvQWNjZXNzUmVxdWVzdEgAEkQKE2dldF9yZXNvdXJjZV9saW1pdHMYFiABKAsyJS5uZXRjbG9kZS52MS5HZXRSZXNvdXJjZUxpbWl0c1JlcXVlc3RIAEIJCgdtZXNzYWdlIsYJCg1TZXJ2ZXJNZXNzYWdlEj4KD3Nlc3Npb25fY3JlYXRlZBgBIAEoCzIjLm5ldGNsb2RlLnYxLlNlc3Npb25DcmVhdGVkUmVzcG9uc2VIABI+Cg9zZXNzaW9uX3VwZGF0ZWQYAiABKAsyIy5uZXRjbG9kZS52MS5TZXNzaW9uVXBkYXRlZFJlc3BvbnNlSAASPgoPc2Vzc2lvbl9kZWxldGVkGAMgASgLMiMubmV0Y2xvZGUudjEuU2Vzc2lvbkRlbGV0ZWRSZXNwb25zZUgAEkcKFHNlc3Npb25zX2RlbGV0ZWRfYWxsGAQgASgLMicubmV0Y2xvZGUudjEuU2Vzc2lvbnNEZWxldGVkQWxsUmVzcG9uc2VIABI4CgxzZXNzaW9uX2xpc3QYBSABKAsyIC5uZXRjbG9kZS52MS5TZXNzaW9uTGlzdFJlc3BvbnNlSAASOgoNc2Vzc2lvbl9zdGF0ZRgGIAEoCzIhLm5ldGNsb2RlLnYxLlNlc3Npb25TdGF0ZVJlc3BvbnNlSAASMgoNc3luY19yZXNwb25zZRgHIAEoCzIZLm5ldGNsb2RlLnYxLlN5bmNSZXNwb25zZUgAEjgKDHN0cmVhbV9lbnRyeRgIIAEoCzIgLm5ldGNsb2RlLnYxLlN0cmVhbUVudHJ5UmVzcG9uc2VIABI4Cgxwb3J0X2V4cG9zZWQYDSABKAsyIC5uZXRjbG9kZS52MS5Qb3J0RXhwb3NlZFJlc3BvbnNlSAASOAoMZ2l0aHViX3JlcG9zGA4gASgLMiAubmV0Y2xvZGUudjEuR2l0SHViUmVwb3NSZXNwb25zZUgAEjQKCmdpdF9zdGF0dXMYDyABKAsyHi5uZXRjbG9kZS52MS5HaXRTdGF0dXNSZXNwb25zZUgAEjAKCGdpdF9kaWZmGBAgASgLMhwubmV0Y2xvZGUudjEuR2l0RGlmZlJlc3BvbnNlSAASKwoFZXJyb3IYESABKAsyGi5uZXRjbG9kZS52MS5FcnJvclJlc3BvbnNlSAASLQoGbW9kZWxzGBIgASgLMhsubmV0Y2xvZGUudjEuTW9kZWxzUmVzcG9uc2VIABI8Cg5jb3BpbG90X3N0YXR1cxgTIAEoCzIiLm5ldGNsb2RlLnYxLkNvcGlsb3RTdGF0dXNSZXNwb25zZUgAEkAKEHNuYXBzaG90X2NyZWF0ZWQYFCABKAsyJC5uZXRjbG9kZS52MS5TbmFwc2hvdENyZWF0ZWRSZXNwb25zZUgAEjoKDXNuYXBzaG90X2xpc3QYFSABKAsyIS5uZXRjbG9kZS52MS5TbmFwc2hvdExpc3RSZXNwb25zZUgAEkIKEXNuYXBzaG90X3Jlc3RvcmVkGBYgASgLMiUubmV0Y2xvZGUudjEuU25hcHNob3RSZXN0b3JlZFJlc3BvbnNlSAASRQoTcmVwb19hY2Nlc3NfdXBkYXRlZBgXIAEoCzImLm5ldGNsb2RlLnYxLlJlcG9BY2Nlc3NVcGRhdGVkUmVzcG9uc2VIABI+Cg9yZXNvdXJjZV9saW1pdHMYGCABKAsyIy5uZXRjbG9kZS52MS5SZXNvdXJjZUxpbWl0c1Jlc3BvbnNlSABCCQoHbWVzc2FnZSInCg1OZXR3b3JrQ29uZmlnEhYKDnRhaWxuZXRfYWNjZXNzGAEgASgIIpQEChRDcmVhdGVTZXNzaW9uUmVxdWVzdBIXCgpyZXF1ZXN0X2lkGAEgASgJSACIAQESEQoEbmFtZRgCIAEoCUgBiAEBEg0KBXJlcG9zGAMgAygJEjEKC3JlcG9fYWNjZXNzGAQgASgOMhcubmV0Y2xvZGUudjEuUmVwb0FjY2Vzc0gCiAEBEhsKDmluaXRpYWxfcHJvbXB0GAUgASgJSAOIAQESKwoIc2RrX3R5cGUYBiABKA4yFC5uZXRjbG9kZS52MS5TZGtUeXBlSASIAQESEgoFbW9kZWwYByABKAlIBYgBARI5Cg9jb3BpbG90X2JhY2tlbmQYCCABKA4yGy5uZXRjbG9kZS52MS5Db3BpbG90QmFja2VuZEgGiAEBEjcKDm5ldHdvcmtfY29uZmlnGAkgASgLMhoubmV0Y2xvZGUudjEuTmV0d29ya0NvbmZpZ0gHiAEBEjUKCXJlc291cmNlcxgKIAEoCzIdLm5ldGNsb2RlLnYxLlNhbmRib3hSZXNvdXJjZXNICIgBAUINCgtfcmVxdWVzdF9pZEIHCgVfbmFtZUIOCgxfcmVwb19hY2Nlc3NCEQoPX2luaXRpYWxfcHJvbXB0QgsKCV9zZGtfdHlwZUIICgZfbW9kZWxCEgoQX2NvcGlsb3RfYmFja2VuZEIRCg9fbmV0d29ya19jb25maWdCDAoKX3Jlc291cmNlcyI9ChNMaXN0U2Vzc2lvbnNSZXF1ZXN0EhcKCnJlcXVlc3RfaWQYASABKAlIAIgBAUINCgtfcmVxdWVzdF9pZCKgAQoST3BlblNlc3Npb25SZXF1ZXN0EhcKCnJlcXVlc3RfaWQYASABKAlIAIgBARISCgpzZXNzaW9uX2lkGAIgASgJEhwKD2FmdGVyX3N0cmVhbV9pZBgDIAEoCUgBiAEBEhIKBWxpbWl0GAQgASgFSAKIAQFCDQoLX3JlcXVlc3RfaWRCEgoQX2FmdGVyX3N0cmVhbV9pZEIICgZfbGltaXQiUgoUUmVzdW1lU2Vzc2lvblJlcXVlc3QSFwoKcmVxdWVzdF9pZBgBIAEoCUgAiAEBEhIKCnNlc3Npb25faWQYAiABKAlCDQoLX3JlcXVlc3RfaWQiUQoTUGF1c2VTZXNzaW9uUmVxdWVzdBIXCgpyZXF1ZXN0X2lkGAEgASgJSACIAQESEgoKc2Vzc2lvbl9pZBgCIAEoCUINCgtfcmVxdWVzdF9pZCJSChREZWxldGVTZXNzaW9uUmVxdWVzdBIXCgpyZXF1ZXN0X2lkGAEgASgJSACIAQESEgoKc2Vzc2lvbl9pZBgCIAEoCUINCgtfcmVxdWVzdF9pZCJCChhEZWxldGVBbGxTZXNzaW9uc1JlcXVlc3QSFwoKcmVxdWVzdF9pZBgBIAEoCUgAiAEBQg0KC19yZXF1ZXN0X2lkIl0KEVNlbmRQcm9tcHRSZXF1ZXN0EhcKCnJlcXVlc3RfaWQYASABKAlIAIgBARISCgpzZXNzaW9uX2lkGAIgASgJEgwKBHRleHQYAyABKAlCDQoLX3JlcXVlc3RfaWQiVAoWSW50ZXJydXB0UHJvbXB0UmVxdWVzdBIXCgpyZXF1ZXN0X2lkGAEgASgJSACIAQESEgoKc2Vzc2lvbl9pZBgCIAEoCUINCgtfcmVxdWVzdF9pZCJgChRUZXJtaW5hbElucHV0UmVxdWVzdBIXCgpyZXF1ZXN0X2lkGAEgASgJSACIAQESEgoKc2Vzc2lvbl9pZBgCIAEoCRIMCgRkYXRhGAMgASgJQg0KC19yZXF1ZXN0X2lkIm8KFVRlcm1pbmFsUmVzaXplUmVxdWVzdBIXCgpyZXF1ZXN0X2lkGAEgASgJSACIAQESEgoKc2Vzc2lvbl9pZBgCIAEoCRIMCgRjb2xzGAMgASgFEgwKBHJvd3MYBCABKAVCDQoLX3JlcXVlc3RfaWQiXQoRRXhwb3NlUG9ydFJlcXVlc3QSFwoKcmVxdWVzdF9pZBgBIAEoCUgAiAEBEhIKCnNlc3Npb25faWQYAiABKAkSDAoEcG9ydBgDIAEoBUINCgtfcmVxdWVzdF9pZCI1CgtTeW5jUmVxdWVzdBIXCgpyZXF1ZXN0X2lkGAEgASgJSACIAQFCDQoLX3JlcXVlc3RfaWQiQAoWTGlzdEdpdEh1YlJlcG9zUmVxdWVzdBIXCgpyZXF1ZXN0X2lkGAEgASgJSACIAQFCDQoLX3JlcXVlc3RfaWQiTgoQR2l0U3RhdHVzUmVxdWVzdBIXCgpyZXF1ZXN0X2lkGAEgASgJSACIAQESEgoKc2Vzc2lvbl9pZBgCIAEoCUINCgtfcmVxdWVzdF9pZCJoCg5HaXREaWZmUmVxdWVzdBIXCgpyZXF1ZXN0X2lkGAEgASgJSACIAQESEgoKc2Vzc2lvbl9pZBgCIAEoCRIRCgRmaWxlGAMgASgJSAGIAQFCDQoLX3JlcXVlc3RfaWRCBwoFX2ZpbGUisgEKEUxpc3RNb2RlbHNSZXF1ZXN0EhcKCnJlcXVlc3RfaWQYASABKAlIAIgBARImCghzZGtfdHlwZRgCIAEoDjIULm5ldGNsb2RlLnYxLlNka1R5cGUSOQoPY29waWxvdF9iYWNrZW5kGAMgASgOMhsubmV0Y2xvZGUudjEuQ29waWxvdEJhY2tlbmRIAYgBAUINCgtfcmVxdWVzdF9pZEISChBfY29waWxvdF9iYWNrZW5kIkEKF0dldENvcGlsb3RTdGF0dXNSZXF1ZXN0EhcKCnJlcXVlc3RfaWQYASABKAlIAIgBAUINCgtfcmVxdWVzdF9pZCJSChRMaXN0U25hcHNob3RzUmVxdWVzdBIXCgpyZXF1ZXN0X2lkGAEgASgJSACIAQESEgoKc2Vzc2lvbl9pZBgCIAEoCUINCgtfcmVxdWVzdF9pZCJpChZSZXN0b3JlU25hcHNob3RSZXF1ZXN0EhcKCnJlcXVlc3RfaWQYASABKAlIAIgBARISCgpzZXNzaW9uX2lkGAIgASgJEhMKC3NuYXBzaG90X2lkGAMgASgJQg0KC19yZXF1ZXN0X2lkIoMBChdVcGRhdGVSZXBvQWNjZXNzUmVxdWVzdBIXCgpyZXF1ZXN0X2lkGAEgASgJSACIAQESEgoKc2Vzc2lvbl9pZBgCIAEoCRIsCgtyZXBvX2FjY2VzcxgDIAEoDjIXLm5ldGNsb2RlLnYxLlJlcG9BY2Nlc3NCDQoLX3JlcXVlc3RfaWQiQgoYR2V0UmVzb3VyY2VMaW1pdHNSZXF1ZXN0EhcKCnJlcXVlc3RfaWQYASABKAlIAIgBAUINCgtfcmVxdWVzdF9pZCJnChZTZXNzaW9uQ3JlYXRlZFJlc3BvbnNlEiUKB3Nlc3Npb24YASABKAsyFC5uZXRjbG9kZS52MS5TZXNzaW9uEhcKCnJlcXVlc3RfaWQYAiABKAlIAIgBAUINCgtfcmVxdWVzdF9pZCI/ChZTZXNzaW9uVXBkYXRlZFJlc3BvbnNlEiUKB3Nlc3Npb24YASABKAsyFC5uZXRjbG9kZS52MS5TZXNzaW9uIlQKFlNlc3Npb25EZWxldGVkUmVzcG9uc2USEgoKc2Vzc2lvbl9pZBgBIAEoCRIXCgpyZXF1ZXN0X2lkGAIgASgJSACIAQFCDQoLX3JlcXVlc3RfaWQiWQoaU2Vzc2lvbnNEZWxldGVkQWxsUmVzcG9uc2USEwoLZGVsZXRlZF9pZHMYASADKAkSFwoKcmVxdWVzdF9pZBgCIAEoCUgAiAEBQg0KC19yZXF1ZXN0X2lkImUKE1Nlc3Npb25MaXN0UmVzcG9uc2USJgoIc2Vzc2lvbnMYASADKAsyFC5uZXRjbG9kZS52MS5TZXNzaW9uEhcKCnJlcXVlc3RfaWQYAiABKAlIAIgBAUINCgtfcmVxdWVzdF9pZCKaAgoUU2Vzc2lvblN0YXRlUmVzcG9uc2USJQoHc2Vzc2lvbhgBIAEoCzIULm5ldGNsb2RlLnYxLlNlc3Npb24SKQoHZW50cmllcxgCIAMoCzIYLm5ldGNsb2RlLnYxLlN0cmVhbUVudHJ5EhAKCGhhc19tb3JlGAMgASgIEhsKDmxhc3Rfc3RyZWFtX2lkGAQgASgJSACIAQESNgoLaW5fcHJvZ3Jlc3MYBSABKAsyHC5uZXRjbG9kZS52MS5JblByb2dyZXNzU3RhdGVIAYgBARIXCgpyZXF1ZXN0X2lkGAYgASgJSAKIAQFCEQoPX2xhc3Rfc3RyZWFtX2lkQg4KDF9pbl9wcm9ncmVzc0INCgtfcmVxdWVzdF9pZCKWAQoMU3luY1Jlc3BvbnNlEi0KCHNlc3Npb25zGAEgAygLMhsubmV0Y2xvZGUudjEuU2Vzc2lvblN1bW1hcnkSLwoLc2VydmVyX3RpbWUYAiABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEhcKCnJlcXVlc3RfaWQYAyABKAlIAIgBAUINCgtfcmVxdWVzdF9pZCJSChNTdHJlYW1FbnRyeVJlc3BvbnNlEhIKCnNlc3Npb25faWQYASABKAkSJwoFZW50cnkYAiABKAsyGC5uZXRjbG9kZS52MS5TdHJlYW1FbnRyeSJ0ChNQb3J0RXhwb3NlZFJlc3BvbnNlEhIKCnNlc3Npb25faWQYASABKAkSDAoEcG9ydBgCIAEoBRITCgtwcmV2aWV3X3VybBgDIAEoCRIXCgpyZXF1ZXN0X2lkGAQgASgJSACIAQFCDQoLX3JlcXVlc3RfaWQiZQoTR2l0SHViUmVwb3NSZXNwb25zZRImCgVyZXBvcxgBIAMoCzIXLm5ldGNsb2RlLnYxLkdpdEh1YlJlcG8SFwoKcmVxdWVzdF9pZBgCIAEoCUgAiAEBQg0KC19yZXF1ZXN0X2lkInoKEUdpdFN0YXR1c1Jlc3BvbnNlEhIKCnNlc3Npb25faWQYASABKAkSKQoFZmlsZXMYAiADKAsyGi5uZXRjbG9kZS52MS5HaXRGaWxlQ2hhbmdlEhcKCnJlcXVlc3RfaWQYAyABKAlIAIgBAUINCgtfcmVxdWVzdF9pZCJbCg9HaXREaWZmUmVzcG9uc2USEgoKc2Vzc2lvbl9pZBgBIAEoCRIMCgRkaWZmGAIgASgJEhcKCnJlcXVlc3RfaWQYAyABKAlIAIgBAUINCgtfcmVxdWVzdF9pZCJaCg1FcnJvclJlc3BvbnNlEiEKBWVycm9yGAEgASgLMhIubmV0Y2xvZGUudjEuRXJyb3ISFwoKcmVxdWVzdF9pZBgCIAEoCUgAiAEBQg0KC19yZXF1ZXN0X2lkIpoBCg5Nb2RlbHNSZXNwb25zZRImCgZtb2RlbHMYASADKAsyFi5uZXRjbG9kZS52MS5Nb2RlbEluZm8SFwoKcmVxdWVzdF9pZBgCIAEoCUgAiAEBEisKCHNka190eXBlGAMgASgOMhQubmV0Y2xvZGUudjEuU2RrVHlwZUgBiAEBQg0KC19yZXF1ZXN0X2lkQgsKCV9zZGtfdHlwZSKtAQoVQ29waWxvdFN0YXR1c1Jlc3BvbnNlEiwKBGF1dGgYASABKAsyHi5uZXRjbG9kZS52MS5Db3BpbG90QXV0aFN0YXR1cxI0CgVxdW90YRgCIAEoCzIgLm5ldGNsb2RlLnYxLkNvcGlsb3RQcmVtaXVtUXVvdGFIAIgBARIXCgpyZXF1ZXN0X2lkGAMgASgJSAGIAQFCCAoGX3F1b3RhQg0KC19yZXF1ZXN0X2lkIlYKF1NuYXBzaG90Q3JlYXRlZFJlc3BvbnNlEhIKCnNlc3Npb25faWQYASABKAkSJwoIc25hcHNob3QYAiABKAsyFS5uZXRjbG9kZS52MS5TbmFwc2hvdCJ8ChRTbmFwc2hvdExpc3RSZXNwb25zZRISCgpzZXNzaW9uX2lkGAEgASgJEigKCXNuYXBzaG90cxgCIAMoCzIVLm5ldGNsb2RlLnYxLlNuYXBzaG90EhcKCnJlcXVlc3RfaWQYAyABKAlIAIgBAUINCgtfcmVxdWVzdF9pZCKGAQoYU25hcHNob3RSZXN0b3JlZFJlc3BvbnNlEhIKCnNlc3Npb25faWQYASABKAkSEwoLc25hcHNob3RfaWQYAiABKAkSGQoRbWVzc2FnZXNfcmVzdG9yZWQYAyABKAUSFwoKcmVxdWVzdF9pZBgEIAEoCUgAiAEBQg0KC19yZXF1ZXN0X2lkIoUBChlSZXBvQWNjZXNzVXBkYXRlZFJlc3BvbnNlEhIKCnNlc3Npb25faWQYASABKAkSLAoLcmVwb19hY2Nlc3MYAiABKA4yFy5uZXRjbG9kZS52MS5SZXBvQWNjZXNzEhcKCnJlcXVlc3RfaWQYAyABKAlIAIgBAUINCgtfcmVxdWVzdF9pZCKcAQoWUmVzb3VyY2VMaW1pdHNSZXNwb25zZRIRCgltYXhfdmNwdXMYASABKAUSFQoNbWF4X21lbW9yeV9tYhgCIAEoBRIVCg1kZWZhdWx0X3ZjcHVzGAMgASgFEhkKEWRlZmF1bHRfbWVtb3J5X21iGAQgASgFEhcKCnJlcXVlc3RfaWQYBSABKAlIAIgBAUINCgtfcmVxdWVzdF9pZDJWCg1DbGllbnRTZXJ2aWNlEkUKB0Nvbm5lY3QSGi5uZXRjbG9kZS52MS5DbGllbnRNZXNzYWdlGhoubmV0Y2xvZGUudjEuU2VydmVyTWVzc2FnZSgBMAFCvAEKD2NvbS5uZXRjbG9kZS52MUILQ2xpZW50UHJvdG9QAVpPZ2l0aHViLmNvbS9hbmdyaXN0YW4vbmV0Y2xvZGUvc2VydmljZXMvY29udHJvbC1wbGFuZS9nZW4vbmV0Y2xvZGUvdjE7bmV0Y2xvZGV2MaICA05YWKoCC05ldGNsb2RlLlYxygILTmV0Y2xvZGVcVjHiAhdOZXRjbG9kZVxWMVxHUEJNZXRhZGF0YeoCDE5ldGNsb2RlOjpWMWIGcHJvdG8z", [file_netclode_v1_common, file_netclode_v1_events, file_google_protobuf_timestamp]); + fileDesc("ChhuZXRjbG9kZS92MS9jbGllbnQucHJvdG8SC25ldGNsb2RlLnYxIsUMCg1DbGllbnRNZXNzYWdlEjsKDmNyZWF0ZV9zZXNzaW9uGAEgASgLMiEubmV0Y2xvZGUudjEuQ3JlYXRlU2Vzc2lvblJlcXVlc3RIABI5Cg1saXN0X3Nlc3Npb25zGAIgASgLMiAubmV0Y2xvZGUudjEuTGlzdFNlc3Npb25zUmVxdWVzdEgAEjcKDG9wZW5fc2Vzc2lvbhgDIAEoCzIfLm5ldGNsb2RlLnYxLk9wZW5TZXNzaW9uUmVxdWVzdEgAEjsKDnJlc3VtZV9zZXNzaW9uGAQgASgLMiEubmV0Y2xvZGUudjEuUmVzdW1lU2Vzc2lvblJlcXVlc3RIABI5Cg1wYXVzZV9zZXNzaW9uGAUgASgLMiAubmV0Y2xvZGUudjEuUGF1c2VTZXNzaW9uUmVxdWVzdEgAEjsKDmRlbGV0ZV9zZXNzaW9uGAYgASgLMiEubmV0Y2xvZGUudjEuRGVsZXRlU2Vzc2lvblJlcXVlc3RIABJEChNkZWxldGVfYWxsX3Nlc3Npb25zGAcgASgLMiUubmV0Y2xvZGUudjEuRGVsZXRlQWxsU2Vzc2lvbnNSZXF1ZXN0SAASNQoLc2VuZF9wcm9tcHQYCCABKAsyHi5uZXRjbG9kZS52MS5TZW5kUHJvbXB0UmVxdWVzdEgAEj8KEGludGVycnVwdF9wcm9tcHQYCSABKAsyIy5uZXRjbG9kZS52MS5JbnRlcnJ1cHRQcm9tcHRSZXF1ZXN0SAASOwoOdGVybWluYWxfaW5wdXQYCiABKAsyIS5uZXRjbG9kZS52MS5UZXJtaW5hbElucHV0UmVxdWVzdEgAEj0KD3Rlcm1pbmFsX3Jlc2l6ZRgLIAEoCzIiLm5ldGNsb2RlLnYxLlRlcm1pbmFsUmVzaXplUmVxdWVzdEgAEjUKC2V4cG9zZV9wb3J0GAwgASgLMh4ubmV0Y2xvZGUudjEuRXhwb3NlUG9ydFJlcXVlc3RIABIoCgRzeW5jGA0gASgLMhgubmV0Y2xvZGUudjEuU3luY1JlcXVlc3RIABJAChFsaXN0X2dpdGh1Yl9yZXBvcxgOIAEoCzIjLm5ldGNsb2RlLnYxLkxpc3RHaXRIdWJSZXBvc1JlcXVlc3RIABIzCgpnaXRfc3RhdHVzGA8gASgLMh0ubmV0Y2xvZGUudjEuR2l0U3RhdHVzUmVxdWVzdEgAEi8KCGdpdF9kaWZmGBAgASgLMhsubmV0Y2xvZGUudjEuR2l0RGlmZlJlcXVlc3RIABI1CgtsaXN0X21vZGVscxgRIAEoCzIeLm5ldGNsb2RlLnYxLkxpc3RNb2RlbHNSZXF1ZXN0SAASQgoSZ2V0X2NvcGlsb3Rfc3RhdHVzGBIgASgLMiQubmV0Y2xvZGUudjEuR2V0Q29waWxvdFN0YXR1c1JlcXVlc3RIABI7Cg5saXN0X3NuYXBzaG90cxgTIAEoCzIhLm5ldGNsb2RlLnYxLkxpc3RTbmFwc2hvdHNSZXF1ZXN0SAASPwoQcmVzdG9yZV9zbmFwc2hvdBgUIAEoCzIjLm5ldGNsb2RlLnYxLlJlc3RvcmVTbmFwc2hvdFJlcXVlc3RIABJCChJ1cGRhdGVfcmVwb19hY2Nlc3MYFSABKAsyJC5uZXRjbG9kZS52MS5VcGRhdGVSZXBvQWNjZXNzUmVxdWVzdEgAEkQKE2dldF9yZXNvdXJjZV9saW1pdHMYFiABKAsyJS5uZXRjbG9kZS52MS5HZXRSZXNvdXJjZUxpbWl0c1JlcXVlc3RIABI+ChBjb2RleF9hdXRoX3N0YXJ0GBcgASgLMiIubmV0Y2xvZGUudjEuQ29kZXhBdXRoU3RhcnRSZXF1ZXN0SAASQAoRY29kZXhfYXV0aF9zdGF0dXMYGCABKAsyIy5uZXRjbG9kZS52MS5Db2RleEF1dGhTdGF0dXNSZXF1ZXN0SAASQAoRY29kZXhfYXV0aF9sb2dvdXQYGSABKAsyIy5uZXRjbG9kZS52MS5Db2RleEF1dGhMb2dvdXRSZXF1ZXN0SAASOQoNdW5leHBvc2VfcG9ydBgaIAEoCzIgLm5ldGNsb2RlLnYxLlVuZXhwb3NlUG9ydFJlcXVlc3RIAEIJCgdtZXNzYWdlItYLCg1TZXJ2ZXJNZXNzYWdlEj4KD3Nlc3Npb25fY3JlYXRlZBgBIAEoCzIjLm5ldGNsb2RlLnYxLlNlc3Npb25DcmVhdGVkUmVzcG9uc2VIABI+Cg9zZXNzaW9uX3VwZGF0ZWQYAiABKAsyIy5uZXRjbG9kZS52MS5TZXNzaW9uVXBkYXRlZFJlc3BvbnNlSAASPgoPc2Vzc2lvbl9kZWxldGVkGAMgASgLMiMubmV0Y2xvZGUudjEuU2Vzc2lvbkRlbGV0ZWRSZXNwb25zZUgAEkcKFHNlc3Npb25zX2RlbGV0ZWRfYWxsGAQgASgLMicubmV0Y2xvZGUudjEuU2Vzc2lvbnNEZWxldGVkQWxsUmVzcG9uc2VIABI4CgxzZXNzaW9uX2xpc3QYBSABKAsyIC5uZXRjbG9kZS52MS5TZXNzaW9uTGlzdFJlc3BvbnNlSAASOgoNc2Vzc2lvbl9zdGF0ZRgGIAEoCzIhLm5ldGNsb2RlLnYxLlNlc3Npb25TdGF0ZVJlc3BvbnNlSAASMgoNc3luY19yZXNwb25zZRgHIAEoCzIZLm5ldGNsb2RlLnYxLlN5bmNSZXNwb25zZUgAEjgKDHN0cmVhbV9lbnRyeRgIIAEoCzIgLm5ldGNsb2RlLnYxLlN0cmVhbUVudHJ5UmVzcG9uc2VIABI4Cgxwb3J0X2V4cG9zZWQYDSABKAsyIC5uZXRjbG9kZS52MS5Qb3J0RXhwb3NlZFJlc3BvbnNlSAASOAoMZ2l0aHViX3JlcG9zGA4gASgLMiAubmV0Y2xvZGUudjEuR2l0SHViUmVwb3NSZXNwb25zZUgAEjQKCmdpdF9zdGF0dXMYDyABKAsyHi5uZXRjbG9kZS52MS5HaXRTdGF0dXNSZXNwb25zZUgAEjAKCGdpdF9kaWZmGBAgASgLMhwubmV0Y2xvZGUudjEuR2l0RGlmZlJlc3BvbnNlSAASKwoFZXJyb3IYESABKAsyGi5uZXRjbG9kZS52MS5FcnJvclJlc3BvbnNlSAASLQoGbW9kZWxzGBIgASgLMhsubmV0Y2xvZGUudjEuTW9kZWxzUmVzcG9uc2VIABI8Cg5jb3BpbG90X3N0YXR1cxgTIAEoCzIiLm5ldGNsb2RlLnYxLkNvcGlsb3RTdGF0dXNSZXNwb25zZUgAEkAKEHNuYXBzaG90X2NyZWF0ZWQYFCABKAsyJC5uZXRjbG9kZS52MS5TbmFwc2hvdENyZWF0ZWRSZXNwb25zZUgAEjoKDXNuYXBzaG90X2xpc3QYFSABKAsyIS5uZXRjbG9kZS52MS5TbmFwc2hvdExpc3RSZXNwb25zZUgAEkIKEXNuYXBzaG90X3Jlc3RvcmVkGBYgASgLMiUubmV0Y2xvZGUudjEuU25hcHNob3RSZXN0b3JlZFJlc3BvbnNlSAASRQoTcmVwb19hY2Nlc3NfdXBkYXRlZBgXIAEoCzImLm5ldGNsb2RlLnYxLlJlcG9BY2Nlc3NVcGRhdGVkUmVzcG9uc2VIABI+Cg9yZXNvdXJjZV9saW1pdHMYGCABKAsyIy5uZXRjbG9kZS52MS5SZXNvdXJjZUxpbWl0c1Jlc3BvbnNlSAASQwoSY29kZXhfYXV0aF9zdGFydGVkGBkgASgLMiUubmV0Y2xvZGUudjEuQ29kZXhBdXRoU3RhcnRlZFJlc3BvbnNlSAASQQoRY29kZXhfYXV0aF9zdGF0dXMYGiABKAsyJC5uZXRjbG9kZS52MS5Db2RleEF1dGhTdGF0dXNSZXNwb25zZUgAEkgKFWNvZGV4X2F1dGhfbG9nZ2VkX291dBgbIAEoCzInLm5ldGNsb2RlLnYxLkNvZGV4QXV0aExvZ2dlZE91dFJlc3BvbnNlSAASPAoOcG9ydF91bmV4cG9zZWQYHCABKAsyIi5uZXRjbG9kZS52MS5Qb3J0VW5leHBvc2VkUmVzcG9uc2VIAEIJCgdtZXNzYWdlIicKDU5ldHdvcmtDb25maWcSFgoOdGFpbG5ldF9hY2Nlc3MYASABKAgilQEKEENvZGV4T0F1dGhUb2tlbnMSFAoMYWNjZXNzX3Rva2VuGAEgASgJEhAKCGlkX3Rva2VuGAIgASgJEhUKDXJlZnJlc2hfdG9rZW4YAyABKAkSMwoKZXhwaXJlc19hdBgEIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXBIAIgBAUINCgtfZXhwaXJlc19hdCLrBAoUQ3JlYXRlU2Vzc2lvblJlcXVlc3QSFwoKcmVxdWVzdF9pZBgBIAEoCUgAiAEBEhEKBG5hbWUYAiABKAlIAYgBARINCgVyZXBvcxgDIAMoCRIxCgtyZXBvX2FjY2VzcxgEIAEoDjIXLm5ldGNsb2RlLnYxLlJlcG9BY2Nlc3NIAogBARIbCg5pbml0aWFsX3Byb21wdBgFIAEoCUgDiAEBEisKCHNka190eXBlGAYgASgOMhQubmV0Y2xvZGUudjEuU2RrVHlwZUgEiAEBEhIKBW1vZGVsGAcgASgJSAWIAQESOQoPY29waWxvdF9iYWNrZW5kGAggASgOMhsubmV0Y2xvZGUudjEuQ29waWxvdEJhY2tlbmRIBogBARI3Cg5uZXR3b3JrX2NvbmZpZxgJIAEoCzIaLm5ldGNsb2RlLnYxLk5ldHdvcmtDb25maWdIB4gBARI1CglyZXNvdXJjZXMYCiABKAsyHS5uZXRjbG9kZS52MS5TYW5kYm94UmVzb3VyY2VzSAiIAQESPgoSY29kZXhfb2F1dGhfdG9rZW5zGAsgASgLMh0ubmV0Y2xvZGUudjEuQ29kZXhPQXV0aFRva2Vuc0gJiAEBQg0KC19yZXF1ZXN0X2lkQgcKBV9uYW1lQg4KDF9yZXBvX2FjY2Vzc0IRCg9faW5pdGlhbF9wcm9tcHRCCwoJX3Nka190eXBlQggKBl9tb2RlbEISChBfY29waWxvdF9iYWNrZW5kQhEKD19uZXR3b3JrX2NvbmZpZ0IMCgpfcmVzb3VyY2VzQhUKE19jb2RleF9vYXV0aF90b2tlbnMiPQoTTGlzdFNlc3Npb25zUmVxdWVzdBIXCgpyZXF1ZXN0X2lkGAEgASgJSACIAQFCDQoLX3JlcXVlc3RfaWQioAEKEk9wZW5TZXNzaW9uUmVxdWVzdBIXCgpyZXF1ZXN0X2lkGAEgASgJSACIAQESEgoKc2Vzc2lvbl9pZBgCIAEoCRIcCg9hZnRlcl9zdHJlYW1faWQYAyABKAlIAYgBARISCgVsaW1pdBgEIAEoBUgCiAEBQg0KC19yZXF1ZXN0X2lkQhIKEF9hZnRlcl9zdHJlYW1faWRCCAoGX2xpbWl0IlIKFFJlc3VtZVNlc3Npb25SZXF1ZXN0EhcKCnJlcXVlc3RfaWQYASABKAlIAIgBARISCgpzZXNzaW9uX2lkGAIgASgJQg0KC19yZXF1ZXN0X2lkIlEKE1BhdXNlU2Vzc2lvblJlcXVlc3QSFwoKcmVxdWVzdF9pZBgBIAEoCUgAiAEBEhIKCnNlc3Npb25faWQYAiABKAlCDQoLX3JlcXVlc3RfaWQiUgoURGVsZXRlU2Vzc2lvblJlcXVlc3QSFwoKcmVxdWVzdF9pZBgBIAEoCUgAiAEBEhIKCnNlc3Npb25faWQYAiABKAlCDQoLX3JlcXVlc3RfaWQiQgoYRGVsZXRlQWxsU2Vzc2lvbnNSZXF1ZXN0EhcKCnJlcXVlc3RfaWQYASABKAlIAIgBAUINCgtfcmVxdWVzdF9pZCJdChFTZW5kUHJvbXB0UmVxdWVzdBIXCgpyZXF1ZXN0X2lkGAEgASgJSACIAQESEgoKc2Vzc2lvbl9pZBgCIAEoCRIMCgR0ZXh0GAMgASgJQg0KC19yZXF1ZXN0X2lkIlQKFkludGVycnVwdFByb21wdFJlcXVlc3QSFwoKcmVxdWVzdF9pZBgBIAEoCUgAiAEBEhIKCnNlc3Npb25faWQYAiABKAlCDQoLX3JlcXVlc3RfaWQiYAoUVGVybWluYWxJbnB1dFJlcXVlc3QSFwoKcmVxdWVzdF9pZBgBIAEoCUgAiAEBEhIKCnNlc3Npb25faWQYAiABKAkSDAoEZGF0YRgDIAEoCUINCgtfcmVxdWVzdF9pZCJvChVUZXJtaW5hbFJlc2l6ZVJlcXVlc3QSFwoKcmVxdWVzdF9pZBgBIAEoCUgAiAEBEhIKCnNlc3Npb25faWQYAiABKAkSDAoEY29scxgDIAEoBRIMCgRyb3dzGAQgASgFQg0KC19yZXF1ZXN0X2lkIl0KEUV4cG9zZVBvcnRSZXF1ZXN0EhcKCnJlcXVlc3RfaWQYASABKAlIAIgBARISCgpzZXNzaW9uX2lkGAIgASgJEgwKBHBvcnQYAyABKAVCDQoLX3JlcXVlc3RfaWQiXwoTVW5leHBvc2VQb3J0UmVxdWVzdBIXCgpyZXF1ZXN0X2lkGAEgASgJSACIAQESEgoKc2Vzc2lvbl9pZBgCIAEoCRIMCgRwb3J0GAMgASgFQg0KC19yZXF1ZXN0X2lkIjUKC1N5bmNSZXF1ZXN0EhcKCnJlcXVlc3RfaWQYASABKAlIAIgBAUINCgtfcmVxdWVzdF9pZCJAChZMaXN0R2l0SHViUmVwb3NSZXF1ZXN0EhcKCnJlcXVlc3RfaWQYASABKAlIAIgBAUINCgtfcmVxdWVzdF9pZCJOChBHaXRTdGF0dXNSZXF1ZXN0EhcKCnJlcXVlc3RfaWQYASABKAlIAIgBARISCgpzZXNzaW9uX2lkGAIgASgJQg0KC19yZXF1ZXN0X2lkImgKDkdpdERpZmZSZXF1ZXN0EhcKCnJlcXVlc3RfaWQYASABKAlIAIgBARISCgpzZXNzaW9uX2lkGAIgASgJEhEKBGZpbGUYAyABKAlIAYgBAUINCgtfcmVxdWVzdF9pZEIHCgVfZmlsZSLwAQoRTGlzdE1vZGVsc1JlcXVlc3QSFwoKcmVxdWVzdF9pZBgBIAEoCUgAiAEBEiYKCHNka190eXBlGAIgASgOMhQubmV0Y2xvZGUudjEuU2RrVHlwZRI5Cg9jb3BpbG90X2JhY2tlbmQYAyABKA4yGy5uZXRjbG9kZS52MS5Db3BpbG90QmFja2VuZEgBiAEBEiIKFWNvZGV4X29hdXRoX2F2YWlsYWJsZRgEIAEoCEgCiAEBQg0KC19yZXF1ZXN0X2lkQhIKEF9jb3BpbG90X2JhY2tlbmRCGAoWX2NvZGV4X29hdXRoX2F2YWlsYWJsZSJBChdHZXRDb3BpbG90U3RhdHVzUmVxdWVzdBIXCgpyZXF1ZXN0X2lkGAEgASgJSACIAQFCDQoLX3JlcXVlc3RfaWQiUgoUTGlzdFNuYXBzaG90c1JlcXVlc3QSFwoKcmVxdWVzdF9pZBgBIAEoCUgAiAEBEhIKCnNlc3Npb25faWQYAiABKAlCDQoLX3JlcXVlc3RfaWQiaQoWUmVzdG9yZVNuYXBzaG90UmVxdWVzdBIXCgpyZXF1ZXN0X2lkGAEgASgJSACIAQESEgoKc2Vzc2lvbl9pZBgCIAEoCRITCgtzbmFwc2hvdF9pZBgDIAEoCUINCgtfcmVxdWVzdF9pZCKDAQoXVXBkYXRlUmVwb0FjY2Vzc1JlcXVlc3QSFwoKcmVxdWVzdF9pZBgBIAEoCUgAiAEBEhIKCnNlc3Npb25faWQYAiABKAkSLAoLcmVwb19hY2Nlc3MYAyABKA4yFy5uZXRjbG9kZS52MS5SZXBvQWNjZXNzQg0KC19yZXF1ZXN0X2lkIkIKGEdldFJlc291cmNlTGltaXRzUmVxdWVzdBIXCgpyZXF1ZXN0X2lkGAEgASgJSACIAQFCDQoLX3JlcXVlc3RfaWQiPwoVQ29kZXhBdXRoU3RhcnRSZXF1ZXN0EhcKCnJlcXVlc3RfaWQYASABKAlIAIgBAUINCgtfcmVxdWVzdF9pZCJAChZDb2RleEF1dGhTdGF0dXNSZXF1ZXN0EhcKCnJlcXVlc3RfaWQYASABKAlIAIgBAUINCgtfcmVxdWVzdF9pZCJAChZDb2RleEF1dGhMb2dvdXRSZXF1ZXN0EhcKCnJlcXVlc3RfaWQYASABKAlIAIgBAUINCgtfcmVxdWVzdF9pZCJnChZTZXNzaW9uQ3JlYXRlZFJlc3BvbnNlEiUKB3Nlc3Npb24YASABKAsyFC5uZXRjbG9kZS52MS5TZXNzaW9uEhcKCnJlcXVlc3RfaWQYAiABKAlIAIgBAUINCgtfcmVxdWVzdF9pZCI/ChZTZXNzaW9uVXBkYXRlZFJlc3BvbnNlEiUKB3Nlc3Npb24YASABKAsyFC5uZXRjbG9kZS52MS5TZXNzaW9uIlQKFlNlc3Npb25EZWxldGVkUmVzcG9uc2USEgoKc2Vzc2lvbl9pZBgBIAEoCRIXCgpyZXF1ZXN0X2lkGAIgASgJSACIAQFCDQoLX3JlcXVlc3RfaWQiWQoaU2Vzc2lvbnNEZWxldGVkQWxsUmVzcG9uc2USEwoLZGVsZXRlZF9pZHMYASADKAkSFwoKcmVxdWVzdF9pZBgCIAEoCUgAiAEBQg0KC19yZXF1ZXN0X2lkImUKE1Nlc3Npb25MaXN0UmVzcG9uc2USJgoIc2Vzc2lvbnMYASADKAsyFC5uZXRjbG9kZS52MS5TZXNzaW9uEhcKCnJlcXVlc3RfaWQYAiABKAlIAIgBAUINCgtfcmVxdWVzdF9pZCKaAgoUU2Vzc2lvblN0YXRlUmVzcG9uc2USJQoHc2Vzc2lvbhgBIAEoCzIULm5ldGNsb2RlLnYxLlNlc3Npb24SKQoHZW50cmllcxgCIAMoCzIYLm5ldGNsb2RlLnYxLlN0cmVhbUVudHJ5EhAKCGhhc19tb3JlGAMgASgIEhsKDmxhc3Rfc3RyZWFtX2lkGAQgASgJSACIAQESNgoLaW5fcHJvZ3Jlc3MYBSABKAsyHC5uZXRjbG9kZS52MS5JblByb2dyZXNzU3RhdGVIAYgBARIXCgpyZXF1ZXN0X2lkGAYgASgJSAKIAQFCEQoPX2xhc3Rfc3RyZWFtX2lkQg4KDF9pbl9wcm9ncmVzc0INCgtfcmVxdWVzdF9pZCKWAQoMU3luY1Jlc3BvbnNlEi0KCHNlc3Npb25zGAEgAygLMhsubmV0Y2xvZGUudjEuU2Vzc2lvblN1bW1hcnkSLwoLc2VydmVyX3RpbWUYAiABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEhcKCnJlcXVlc3RfaWQYAyABKAlIAIgBAUINCgtfcmVxdWVzdF9pZCJSChNTdHJlYW1FbnRyeVJlc3BvbnNlEhIKCnNlc3Npb25faWQYASABKAkSJwoFZW50cnkYAiABKAsyGC5uZXRjbG9kZS52MS5TdHJlYW1FbnRyeSJ0ChNQb3J0RXhwb3NlZFJlc3BvbnNlEhIKCnNlc3Npb25faWQYASABKAkSDAoEcG9ydBgCIAEoBRITCgtwcmV2aWV3X3VybBgDIAEoCRIXCgpyZXF1ZXN0X2lkGAQgASgJSACIAQFCDQoLX3JlcXVlc3RfaWQiYQoVUG9ydFVuZXhwb3NlZFJlc3BvbnNlEhIKCnNlc3Npb25faWQYASABKAkSDAoEcG9ydBgCIAEoBRIXCgpyZXF1ZXN0X2lkGAMgASgJSACIAQFCDQoLX3JlcXVlc3RfaWQiZQoTR2l0SHViUmVwb3NSZXNwb25zZRImCgVyZXBvcxgBIAMoCzIXLm5ldGNsb2RlLnYxLkdpdEh1YlJlcG8SFwoKcmVxdWVzdF9pZBgCIAEoCUgAiAEBQg0KC19yZXF1ZXN0X2lkInoKEUdpdFN0YXR1c1Jlc3BvbnNlEhIKCnNlc3Npb25faWQYASABKAkSKQoFZmlsZXMYAiADKAsyGi5uZXRjbG9kZS52MS5HaXRGaWxlQ2hhbmdlEhcKCnJlcXVlc3RfaWQYAyABKAlIAIgBAUINCgtfcmVxdWVzdF9pZCJbCg9HaXREaWZmUmVzcG9uc2USEgoKc2Vzc2lvbl9pZBgBIAEoCRIMCgRkaWZmGAIgASgJEhcKCnJlcXVlc3RfaWQYAyABKAlIAIgBAUINCgtfcmVxdWVzdF9pZCJaCg1FcnJvclJlc3BvbnNlEiEKBWVycm9yGAEgASgLMhIubmV0Y2xvZGUudjEuRXJyb3ISFwoKcmVxdWVzdF9pZBgCIAEoCUgAiAEBQg0KC19yZXF1ZXN0X2lkIpoBCg5Nb2RlbHNSZXNwb25zZRImCgZtb2RlbHMYASADKAsyFi5uZXRjbG9kZS52MS5Nb2RlbEluZm8SFwoKcmVxdWVzdF9pZBgCIAEoCUgAiAEBEisKCHNka190eXBlGAMgASgOMhQubmV0Y2xvZGUudjEuU2RrVHlwZUgBiAEBQg0KC19yZXF1ZXN0X2lkQgsKCV9zZGtfdHlwZSKtAQoVQ29waWxvdFN0YXR1c1Jlc3BvbnNlEiwKBGF1dGgYASABKAsyHi5uZXRjbG9kZS52MS5Db3BpbG90QXV0aFN0YXR1cxI0CgVxdW90YRgCIAEoCzIgLm5ldGNsb2RlLnYxLkNvcGlsb3RQcmVtaXVtUXVvdGFIAIgBARIXCgpyZXF1ZXN0X2lkGAMgASgJSAGIAQFCCAoGX3F1b3RhQg0KC19yZXF1ZXN0X2lkIv8BChhDb2RleEF1dGhTdGFydGVkUmVzcG9uc2USGAoQdmVyaWZpY2F0aW9uX3VyaRgBIAEoCRImChl2ZXJpZmljYXRpb25fdXJpX2NvbXBsZXRlGAIgASgJSACIAQESEQoJdXNlcl9jb2RlGAMgASgJEhgKEGludGVydmFsX3NlY29uZHMYBCABKAUSLgoKZXhwaXJlc19hdBgFIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASFwoKcmVxdWVzdF9pZBgGIAEoCUgBiAEBQhwKGl92ZXJpZmljYXRpb25fdXJpX2NvbXBsZXRlQg0KC19yZXF1ZXN0X2lkIvcBChdDb2RleEF1dGhTdGF0dXNSZXNwb25zZRIqCgVzdGF0ZRgBIAEoDjIbLm5ldGNsb2RlLnYxLkNvZGV4QXV0aFN0YXRlEhcKCmFjY291bnRfaWQYAiABKAlIAIgBARIzCgpleHBpcmVzX2F0GAMgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcEgBiAEBEhIKBWVycm9yGAQgASgJSAKIAQESFwoKcmVxdWVzdF9pZBgFIAEoCUgDiAEBQg0KC19hY2NvdW50X2lkQg0KC19leHBpcmVzX2F0QggKBl9lcnJvckINCgtfcmVxdWVzdF9pZCJEChpDb2RleEF1dGhMb2dnZWRPdXRSZXNwb25zZRIXCgpyZXF1ZXN0X2lkGAEgASgJSACIAQFCDQoLX3JlcXVlc3RfaWQiVgoXU25hcHNob3RDcmVhdGVkUmVzcG9uc2USEgoKc2Vzc2lvbl9pZBgBIAEoCRInCghzbmFwc2hvdBgCIAEoCzIVLm5ldGNsb2RlLnYxLlNuYXBzaG90InwKFFNuYXBzaG90TGlzdFJlc3BvbnNlEhIKCnNlc3Npb25faWQYASABKAkSKAoJc25hcHNob3RzGAIgAygLMhUubmV0Y2xvZGUudjEuU25hcHNob3QSFwoKcmVxdWVzdF9pZBgDIAEoCUgAiAEBQg0KC19yZXF1ZXN0X2lkIoYBChhTbmFwc2hvdFJlc3RvcmVkUmVzcG9uc2USEgoKc2Vzc2lvbl9pZBgBIAEoCRITCgtzbmFwc2hvdF9pZBgCIAEoCRIZChFtZXNzYWdlc19yZXN0b3JlZBgDIAEoBRIXCgpyZXF1ZXN0X2lkGAQgASgJSACIAQFCDQoLX3JlcXVlc3RfaWQihQEKGVJlcG9BY2Nlc3NVcGRhdGVkUmVzcG9uc2USEgoKc2Vzc2lvbl9pZBgBIAEoCRIsCgtyZXBvX2FjY2VzcxgCIAEoDjIXLm5ldGNsb2RlLnYxLlJlcG9BY2Nlc3MSFwoKcmVxdWVzdF9pZBgDIAEoCUgAiAEBQg0KC19yZXF1ZXN0X2lkIpwBChZSZXNvdXJjZUxpbWl0c1Jlc3BvbnNlEhEKCW1heF92Y3B1cxgBIAEoBRIVCg1tYXhfbWVtb3J5X21iGAIgASgFEhUKDWRlZmF1bHRfdmNwdXMYAyABKAUSGQoRZGVmYXVsdF9tZW1vcnlfbWIYBCABKAUSFwoKcmVxdWVzdF9pZBgFIAEoCUgAiAEBQg0KC19yZXF1ZXN0X2lkKq4BCg5Db2RleEF1dGhTdGF0ZRIgChxDT0RFWF9BVVRIX1NUQVRFX1VOU1BFQ0lGSUVEEAASJAogQ09ERVhfQVVUSF9TVEFURV9VTkFVVEhFTlRJQ0FURUQQARIcChhDT0RFWF9BVVRIX1NUQVRFX1BFTkRJTkcQAhIaChZDT0RFWF9BVVRIX1NUQVRFX1JFQURZEAMSGgoWQ09ERVhfQVVUSF9TVEFURV9FUlJPUhAEMlYKDUNsaWVudFNlcnZpY2USRQoHQ29ubmVjdBIaLm5ldGNsb2RlLnYxLkNsaWVudE1lc3NhZ2UaGi5uZXRjbG9kZS52MS5TZXJ2ZXJNZXNzYWdlKAEwAUK8AQoPY29tLm5ldGNsb2RlLnYxQgtDbGllbnRQcm90b1ABWk9naXRodWIuY29tL2FuZ3Jpc3Rhbi9uZXRjbG9kZS9zZXJ2aWNlcy9jb250cm9sLXBsYW5lL2dlbi9uZXRjbG9kZS92MTtuZXRjbG9kZXYxogIDTlhYqgILTmV0Y2xvZGUuVjHKAgtOZXRjbG9kZVxWMeICF05ldGNsb2RlXFYxXEdQQk1ldGFkYXRh6gIMTmV0Y2xvZGU6OlYxYgZwcm90bzM", [file_netclode_v1_common, file_netclode_v1_events, file_google_protobuf_timestamp]); /** * ClientMessage is the union of all client-to-server messages. @@ -164,6 +164,32 @@ export type ClientMessage = Message<"netclode.v1.ClientMessage"> & { */ value: GetResourceLimitsRequest; case: "getResourceLimits"; + } | { + /** + * Backend-managed Codex OAuth flow + * + * @generated from field: netclode.v1.CodexAuthStartRequest codex_auth_start = 23; + */ + value: CodexAuthStartRequest; + case: "codexAuthStart"; + } | { + /** + * @generated from field: netclode.v1.CodexAuthStatusRequest codex_auth_status = 24; + */ + value: CodexAuthStatusRequest; + case: "codexAuthStatus"; + } | { + /** + * @generated from field: netclode.v1.CodexAuthLogoutRequest codex_auth_logout = 25; + */ + value: CodexAuthLogoutRequest; + case: "codexAuthLogout"; + } | { + /** + * @generated from field: netclode.v1.UnexposePortRequest unexpose_port = 26; + */ + value: UnexposePortRequest; + case: "unexposePort"; } | { case: undefined; value?: undefined }; }; @@ -315,6 +341,32 @@ export type ServerMessage = Message<"netclode.v1.ServerMessage"> & { */ value: ResourceLimitsResponse; case: "resourceLimits"; + } | { + /** + * Backend-managed Codex OAuth flow + * + * @generated from field: netclode.v1.CodexAuthStartedResponse codex_auth_started = 25; + */ + value: CodexAuthStartedResponse; + case: "codexAuthStarted"; + } | { + /** + * @generated from field: netclode.v1.CodexAuthStatusResponse codex_auth_status = 26; + */ + value: CodexAuthStatusResponse; + case: "codexAuthStatus"; + } | { + /** + * @generated from field: netclode.v1.CodexAuthLoggedOutResponse codex_auth_logged_out = 27; + */ + value: CodexAuthLoggedOutResponse; + case: "codexAuthLoggedOut"; + } | { + /** + * @generated from field: netclode.v1.PortUnexposedResponse port_unexposed = 28; + */ + value: PortUnexposedResponse; + case: "portUnexposed"; } | { case: undefined; value?: undefined }; }; @@ -348,6 +400,40 @@ export type NetworkConfig = Message<"netclode.v1.NetworkConfig"> & { export const NetworkConfigSchema: GenMessage = /*@__PURE__*/ messageDesc(file_netclode_v1_client, 2); +/** + * CodexOAuthTokens contains ChatGPT OAuth tokens for Codex sessions. + * + * @generated from message netclode.v1.CodexOAuthTokens + */ +export type CodexOAuthTokens = Message<"netclode.v1.CodexOAuthTokens"> & { + /** + * @generated from field: string access_token = 1; + */ + accessToken: string; + + /** + * @generated from field: string id_token = 2; + */ + idToken: string; + + /** + * @generated from field: string refresh_token = 3; + */ + refreshToken: string; + + /** + * @generated from field: optional google.protobuf.Timestamp expires_at = 4; + */ + expiresAt?: Timestamp; +}; + +/** + * Describes the message netclode.v1.CodexOAuthTokens. + * Use `create(CodexOAuthTokensSchema)` to create a new message. + */ +export const CodexOAuthTokensSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_netclode_v1_client, 3); + /** * @generated from message netclode.v1.CreateSessionRequest */ @@ -421,6 +507,13 @@ export type CreateSessionRequest = Message<"netclode.v1.CreateSessionRequest"> & * @generated from field: optional netclode.v1.SandboxResources resources = 10; */ resources?: SandboxResources; + + /** + * Session-scoped OAuth tokens for Codex :oauth models + * + * @generated from field: optional netclode.v1.CodexOAuthTokens codex_oauth_tokens = 11; + */ + codexOauthTokens?: CodexOAuthTokens; }; /** @@ -428,7 +521,7 @@ export type CreateSessionRequest = Message<"netclode.v1.CreateSessionRequest"> & * Use `create(CreateSessionRequestSchema)` to create a new message. */ export const CreateSessionRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_netclode_v1_client, 3); + messageDesc(file_netclode_v1_client, 4); /** * @generated from message netclode.v1.ListSessionsRequest @@ -445,7 +538,7 @@ export type ListSessionsRequest = Message<"netclode.v1.ListSessionsRequest"> & { * Use `create(ListSessionsRequestSchema)` to create a new message. */ export const ListSessionsRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_netclode_v1_client, 4); + messageDesc(file_netclode_v1_client, 5); /** * @generated from message netclode.v1.OpenSessionRequest @@ -481,7 +574,7 @@ export type OpenSessionRequest = Message<"netclode.v1.OpenSessionRequest"> & { * Use `create(OpenSessionRequestSchema)` to create a new message. */ export const OpenSessionRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_netclode_v1_client, 5); + messageDesc(file_netclode_v1_client, 6); /** * @generated from message netclode.v1.ResumeSessionRequest @@ -503,7 +596,7 @@ export type ResumeSessionRequest = Message<"netclode.v1.ResumeSessionRequest"> & * Use `create(ResumeSessionRequestSchema)` to create a new message. */ export const ResumeSessionRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_netclode_v1_client, 6); + messageDesc(file_netclode_v1_client, 7); /** * @generated from message netclode.v1.PauseSessionRequest @@ -525,7 +618,7 @@ export type PauseSessionRequest = Message<"netclode.v1.PauseSessionRequest"> & { * Use `create(PauseSessionRequestSchema)` to create a new message. */ export const PauseSessionRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_netclode_v1_client, 7); + messageDesc(file_netclode_v1_client, 8); /** * @generated from message netclode.v1.DeleteSessionRequest @@ -547,7 +640,7 @@ export type DeleteSessionRequest = Message<"netclode.v1.DeleteSessionRequest"> & * Use `create(DeleteSessionRequestSchema)` to create a new message. */ export const DeleteSessionRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_netclode_v1_client, 8); + messageDesc(file_netclode_v1_client, 9); /** * @generated from message netclode.v1.DeleteAllSessionsRequest @@ -564,7 +657,7 @@ export type DeleteAllSessionsRequest = Message<"netclode.v1.DeleteAllSessionsReq * Use `create(DeleteAllSessionsRequestSchema)` to create a new message. */ export const DeleteAllSessionsRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_netclode_v1_client, 9); + messageDesc(file_netclode_v1_client, 10); /** * @generated from message netclode.v1.SendPromptRequest @@ -591,7 +684,7 @@ export type SendPromptRequest = Message<"netclode.v1.SendPromptRequest"> & { * Use `create(SendPromptRequestSchema)` to create a new message. */ export const SendPromptRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_netclode_v1_client, 10); + messageDesc(file_netclode_v1_client, 11); /** * @generated from message netclode.v1.InterruptPromptRequest @@ -613,7 +706,7 @@ export type InterruptPromptRequest = Message<"netclode.v1.InterruptPromptRequest * Use `create(InterruptPromptRequestSchema)` to create a new message. */ export const InterruptPromptRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_netclode_v1_client, 11); + messageDesc(file_netclode_v1_client, 12); /** * @generated from message netclode.v1.TerminalInputRequest @@ -640,7 +733,7 @@ export type TerminalInputRequest = Message<"netclode.v1.TerminalInputRequest"> & * Use `create(TerminalInputRequestSchema)` to create a new message. */ export const TerminalInputRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_netclode_v1_client, 12); + messageDesc(file_netclode_v1_client, 13); /** * @generated from message netclode.v1.TerminalResizeRequest @@ -672,7 +765,7 @@ export type TerminalResizeRequest = Message<"netclode.v1.TerminalResizeRequest"> * Use `create(TerminalResizeRequestSchema)` to create a new message. */ export const TerminalResizeRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_netclode_v1_client, 13); + messageDesc(file_netclode_v1_client, 14); /** * @generated from message netclode.v1.ExposePortRequest @@ -699,7 +792,34 @@ export type ExposePortRequest = Message<"netclode.v1.ExposePortRequest"> & { * Use `create(ExposePortRequestSchema)` to create a new message. */ export const ExposePortRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_netclode_v1_client, 14); + messageDesc(file_netclode_v1_client, 15); + +/** + * @generated from message netclode.v1.UnexposePortRequest + */ +export type UnexposePortRequest = Message<"netclode.v1.UnexposePortRequest"> & { + /** + * @generated from field: optional string request_id = 1; + */ + requestId?: string; + + /** + * @generated from field: string session_id = 2; + */ + sessionId: string; + + /** + * @generated from field: int32 port = 3; + */ + port: number; +}; + +/** + * Describes the message netclode.v1.UnexposePortRequest. + * Use `create(UnexposePortRequestSchema)` to create a new message. + */ +export const UnexposePortRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_netclode_v1_client, 16); /** * @generated from message netclode.v1.SyncRequest @@ -716,7 +836,7 @@ export type SyncRequest = Message<"netclode.v1.SyncRequest"> & { * Use `create(SyncRequestSchema)` to create a new message. */ export const SyncRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_netclode_v1_client, 15); + messageDesc(file_netclode_v1_client, 17); /** * @generated from message netclode.v1.ListGitHubReposRequest @@ -733,7 +853,7 @@ export type ListGitHubReposRequest = Message<"netclode.v1.ListGitHubReposRequest * Use `create(ListGitHubReposRequestSchema)` to create a new message. */ export const ListGitHubReposRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_netclode_v1_client, 16); + messageDesc(file_netclode_v1_client, 18); /** * @generated from message netclode.v1.GitStatusRequest @@ -755,7 +875,7 @@ export type GitStatusRequest = Message<"netclode.v1.GitStatusRequest"> & { * Use `create(GitStatusRequestSchema)` to create a new message. */ export const GitStatusRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_netclode_v1_client, 17); + messageDesc(file_netclode_v1_client, 19); /** * @generated from message netclode.v1.GitDiffRequest @@ -784,7 +904,7 @@ export type GitDiffRequest = Message<"netclode.v1.GitDiffRequest"> & { * Use `create(GitDiffRequestSchema)` to create a new message. */ export const GitDiffRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_netclode_v1_client, 18); + messageDesc(file_netclode_v1_client, 20); /** * @generated from message netclode.v1.ListModelsRequest @@ -808,6 +928,13 @@ export type ListModelsRequest = Message<"netclode.v1.ListModelsRequest"> & { * @generated from field: optional netclode.v1.CopilotBackend copilot_backend = 3; */ copilotBackend?: CopilotBackend; + + /** + * Hint from client to include Codex :oauth model variants + * + * @generated from field: optional bool codex_oauth_available = 4; + */ + codexOauthAvailable?: boolean; }; /** @@ -815,7 +942,7 @@ export type ListModelsRequest = Message<"netclode.v1.ListModelsRequest"> & { * Use `create(ListModelsRequestSchema)` to create a new message. */ export const ListModelsRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_netclode_v1_client, 19); + messageDesc(file_netclode_v1_client, 21); /** * @generated from message netclode.v1.GetCopilotStatusRequest @@ -832,7 +959,7 @@ export type GetCopilotStatusRequest = Message<"netclode.v1.GetCopilotStatusReque * Use `create(GetCopilotStatusRequestSchema)` to create a new message. */ export const GetCopilotStatusRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_netclode_v1_client, 20); + messageDesc(file_netclode_v1_client, 22); /** * @generated from message netclode.v1.ListSnapshotsRequest @@ -854,7 +981,7 @@ export type ListSnapshotsRequest = Message<"netclode.v1.ListSnapshotsRequest"> & * Use `create(ListSnapshotsRequestSchema)` to create a new message. */ export const ListSnapshotsRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_netclode_v1_client, 21); + messageDesc(file_netclode_v1_client, 23); /** * @generated from message netclode.v1.RestoreSnapshotRequest @@ -881,7 +1008,7 @@ export type RestoreSnapshotRequest = Message<"netclode.v1.RestoreSnapshotRequest * Use `create(RestoreSnapshotRequestSchema)` to create a new message. */ export const RestoreSnapshotRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_netclode_v1_client, 22); + messageDesc(file_netclode_v1_client, 24); /** * @generated from message netclode.v1.UpdateRepoAccessRequest @@ -910,7 +1037,7 @@ export type UpdateRepoAccessRequest = Message<"netclode.v1.UpdateRepoAccessReque * Use `create(UpdateRepoAccessRequestSchema)` to create a new message. */ export const UpdateRepoAccessRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_netclode_v1_client, 23); + messageDesc(file_netclode_v1_client, 25); /** * @generated from message netclode.v1.GetResourceLimitsRequest @@ -927,7 +1054,58 @@ export type GetResourceLimitsRequest = Message<"netclode.v1.GetResourceLimitsReq * Use `create(GetResourceLimitsRequestSchema)` to create a new message. */ export const GetResourceLimitsRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_netclode_v1_client, 24); + messageDesc(file_netclode_v1_client, 26); + +/** + * @generated from message netclode.v1.CodexAuthStartRequest + */ +export type CodexAuthStartRequest = Message<"netclode.v1.CodexAuthStartRequest"> & { + /** + * @generated from field: optional string request_id = 1; + */ + requestId?: string; +}; + +/** + * Describes the message netclode.v1.CodexAuthStartRequest. + * Use `create(CodexAuthStartRequestSchema)` to create a new message. + */ +export const CodexAuthStartRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_netclode_v1_client, 27); + +/** + * @generated from message netclode.v1.CodexAuthStatusRequest + */ +export type CodexAuthStatusRequest = Message<"netclode.v1.CodexAuthStatusRequest"> & { + /** + * @generated from field: optional string request_id = 1; + */ + requestId?: string; +}; + +/** + * Describes the message netclode.v1.CodexAuthStatusRequest. + * Use `create(CodexAuthStatusRequestSchema)` to create a new message. + */ +export const CodexAuthStatusRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_netclode_v1_client, 28); + +/** + * @generated from message netclode.v1.CodexAuthLogoutRequest + */ +export type CodexAuthLogoutRequest = Message<"netclode.v1.CodexAuthLogoutRequest"> & { + /** + * @generated from field: optional string request_id = 1; + */ + requestId?: string; +}; + +/** + * Describes the message netclode.v1.CodexAuthLogoutRequest. + * Use `create(CodexAuthLogoutRequestSchema)` to create a new message. + */ +export const CodexAuthLogoutRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_netclode_v1_client, 29); /** * @generated from message netclode.v1.SessionCreatedResponse @@ -951,7 +1129,7 @@ export type SessionCreatedResponse = Message<"netclode.v1.SessionCreatedResponse * Use `create(SessionCreatedResponseSchema)` to create a new message. */ export const SessionCreatedResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_netclode_v1_client, 25); + messageDesc(file_netclode_v1_client, 30); /** * @generated from message netclode.v1.SessionUpdatedResponse @@ -968,7 +1146,7 @@ export type SessionUpdatedResponse = Message<"netclode.v1.SessionUpdatedResponse * Use `create(SessionUpdatedResponseSchema)` to create a new message. */ export const SessionUpdatedResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_netclode_v1_client, 26); + messageDesc(file_netclode_v1_client, 31); /** * @generated from message netclode.v1.SessionDeletedResponse @@ -990,7 +1168,7 @@ export type SessionDeletedResponse = Message<"netclode.v1.SessionDeletedResponse * Use `create(SessionDeletedResponseSchema)` to create a new message. */ export const SessionDeletedResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_netclode_v1_client, 27); + messageDesc(file_netclode_v1_client, 32); /** * @generated from message netclode.v1.SessionsDeletedAllResponse @@ -1012,7 +1190,7 @@ export type SessionsDeletedAllResponse = Message<"netclode.v1.SessionsDeletedAll * Use `create(SessionsDeletedAllResponseSchema)` to create a new message. */ export const SessionsDeletedAllResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_netclode_v1_client, 28); + messageDesc(file_netclode_v1_client, 33); /** * @generated from message netclode.v1.SessionListResponse @@ -1034,7 +1212,7 @@ export type SessionListResponse = Message<"netclode.v1.SessionListResponse"> & { * Use `create(SessionListResponseSchema)` to create a new message. */ export const SessionListResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_netclode_v1_client, 29); + messageDesc(file_netclode_v1_client, 34); /** * @generated from message netclode.v1.SessionStateResponse @@ -1084,7 +1262,7 @@ export type SessionStateResponse = Message<"netclode.v1.SessionStateResponse"> & * Use `create(SessionStateResponseSchema)` to create a new message. */ export const SessionStateResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_netclode_v1_client, 30); + messageDesc(file_netclode_v1_client, 35); /** * @generated from message netclode.v1.SyncResponse @@ -1111,7 +1289,7 @@ export type SyncResponse = Message<"netclode.v1.SyncResponse"> & { * Use `create(SyncResponseSchema)` to create a new message. */ export const SyncResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_netclode_v1_client, 31); + messageDesc(file_netclode_v1_client, 36); /** * StreamEntryResponse wraps a StreamEntry for real-time push notifications. @@ -1136,7 +1314,7 @@ export type StreamEntryResponse = Message<"netclode.v1.StreamEntryResponse"> & { * Use `create(StreamEntryResponseSchema)` to create a new message. */ export const StreamEntryResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_netclode_v1_client, 32); + messageDesc(file_netclode_v1_client, 37); /** * @generated from message netclode.v1.PortExposedResponse @@ -1168,7 +1346,34 @@ export type PortExposedResponse = Message<"netclode.v1.PortExposedResponse"> & { * Use `create(PortExposedResponseSchema)` to create a new message. */ export const PortExposedResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_netclode_v1_client, 33); + messageDesc(file_netclode_v1_client, 38); + +/** + * @generated from message netclode.v1.PortUnexposedResponse + */ +export type PortUnexposedResponse = Message<"netclode.v1.PortUnexposedResponse"> & { + /** + * @generated from field: string session_id = 1; + */ + sessionId: string; + + /** + * @generated from field: int32 port = 2; + */ + port: number; + + /** + * @generated from field: optional string request_id = 3; + */ + requestId?: string; +}; + +/** + * Describes the message netclode.v1.PortUnexposedResponse. + * Use `create(PortUnexposedResponseSchema)` to create a new message. + */ +export const PortUnexposedResponseSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_netclode_v1_client, 39); /** * @generated from message netclode.v1.GitHubReposResponse @@ -1190,7 +1395,7 @@ export type GitHubReposResponse = Message<"netclode.v1.GitHubReposResponse"> & { * Use `create(GitHubReposResponseSchema)` to create a new message. */ export const GitHubReposResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_netclode_v1_client, 34); + messageDesc(file_netclode_v1_client, 40); /** * @generated from message netclode.v1.GitStatusResponse @@ -1217,7 +1422,7 @@ export type GitStatusResponse = Message<"netclode.v1.GitStatusResponse"> & { * Use `create(GitStatusResponseSchema)` to create a new message. */ export const GitStatusResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_netclode_v1_client, 35); + messageDesc(file_netclode_v1_client, 41); /** * @generated from message netclode.v1.GitDiffResponse @@ -1244,7 +1449,7 @@ export type GitDiffResponse = Message<"netclode.v1.GitDiffResponse"> & { * Use `create(GitDiffResponseSchema)` to create a new message. */ export const GitDiffResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_netclode_v1_client, 36); + messageDesc(file_netclode_v1_client, 42); /** * ErrorResponse is the unified error type for all error conditions. @@ -1273,7 +1478,7 @@ export type ErrorResponse = Message<"netclode.v1.ErrorResponse"> & { * Use `create(ErrorResponseSchema)` to create a new message. */ export const ErrorResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_netclode_v1_client, 37); + messageDesc(file_netclode_v1_client, 43); /** * @generated from message netclode.v1.ModelsResponse @@ -1304,7 +1509,7 @@ export type ModelsResponse = Message<"netclode.v1.ModelsResponse"> & { * Use `create(ModelsResponseSchema)` to create a new message. */ export const ModelsResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_netclode_v1_client, 38); + messageDesc(file_netclode_v1_client, 44); /** * @generated from message netclode.v1.CopilotStatusResponse @@ -1335,7 +1540,103 @@ export type CopilotStatusResponse = Message<"netclode.v1.CopilotStatusResponse"> * Use `create(CopilotStatusResponseSchema)` to create a new message. */ export const CopilotStatusResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_netclode_v1_client, 39); + messageDesc(file_netclode_v1_client, 45); + +/** + * @generated from message netclode.v1.CodexAuthStartedResponse + */ +export type CodexAuthStartedResponse = Message<"netclode.v1.CodexAuthStartedResponse"> & { + /** + * @generated from field: string verification_uri = 1; + */ + verificationUri: string; + + /** + * @generated from field: optional string verification_uri_complete = 2; + */ + verificationUriComplete?: string; + + /** + * @generated from field: string user_code = 3; + */ + userCode: string; + + /** + * @generated from field: int32 interval_seconds = 4; + */ + intervalSeconds: number; + + /** + * @generated from field: google.protobuf.Timestamp expires_at = 5; + */ + expiresAt?: Timestamp; + + /** + * @generated from field: optional string request_id = 6; + */ + requestId?: string; +}; + +/** + * Describes the message netclode.v1.CodexAuthStartedResponse. + * Use `create(CodexAuthStartedResponseSchema)` to create a new message. + */ +export const CodexAuthStartedResponseSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_netclode_v1_client, 46); + +/** + * @generated from message netclode.v1.CodexAuthStatusResponse + */ +export type CodexAuthStatusResponse = Message<"netclode.v1.CodexAuthStatusResponse"> & { + /** + * @generated from field: netclode.v1.CodexAuthState state = 1; + */ + state: CodexAuthState; + + /** + * @generated from field: optional string account_id = 2; + */ + accountId?: string; + + /** + * @generated from field: optional google.protobuf.Timestamp expires_at = 3; + */ + expiresAt?: Timestamp; + + /** + * @generated from field: optional string error = 4; + */ + error?: string; + + /** + * @generated from field: optional string request_id = 5; + */ + requestId?: string; +}; + +/** + * Describes the message netclode.v1.CodexAuthStatusResponse. + * Use `create(CodexAuthStatusResponseSchema)` to create a new message. + */ +export const CodexAuthStatusResponseSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_netclode_v1_client, 47); + +/** + * @generated from message netclode.v1.CodexAuthLoggedOutResponse + */ +export type CodexAuthLoggedOutResponse = Message<"netclode.v1.CodexAuthLoggedOutResponse"> & { + /** + * @generated from field: optional string request_id = 1; + */ + requestId?: string; +}; + +/** + * Describes the message netclode.v1.CodexAuthLoggedOutResponse. + * Use `create(CodexAuthLoggedOutResponseSchema)` to create a new message. + */ +export const CodexAuthLoggedOutResponseSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_netclode_v1_client, 48); /** * SnapshotCreatedResponse is pushed to clients when an auto-snapshot is created after a turn. @@ -1359,7 +1660,7 @@ export type SnapshotCreatedResponse = Message<"netclode.v1.SnapshotCreatedRespon * Use `create(SnapshotCreatedResponseSchema)` to create a new message. */ export const SnapshotCreatedResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_netclode_v1_client, 40); + messageDesc(file_netclode_v1_client, 49); /** * @generated from message netclode.v1.SnapshotListResponse @@ -1388,7 +1689,7 @@ export type SnapshotListResponse = Message<"netclode.v1.SnapshotListResponse"> & * Use `create(SnapshotListResponseSchema)` to create a new message. */ export const SnapshotListResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_netclode_v1_client, 41); + messageDesc(file_netclode_v1_client, 50); /** * SnapshotRestoredResponse is sent after workspace and messages are restored. @@ -1424,7 +1725,7 @@ export type SnapshotRestoredResponse = Message<"netclode.v1.SnapshotRestoredResp * Use `create(SnapshotRestoredResponseSchema)` to create a new message. */ export const SnapshotRestoredResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_netclode_v1_client, 42); + messageDesc(file_netclode_v1_client, 51); /** * RepoAccessUpdatedResponse is sent after repo access level is updated. @@ -1455,7 +1756,7 @@ export type RepoAccessUpdatedResponse = Message<"netclode.v1.RepoAccessUpdatedRe * Use `create(RepoAccessUpdatedResponseSchema)` to create a new message. */ export const RepoAccessUpdatedResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_netclode_v1_client, 43); + messageDesc(file_netclode_v1_client, 52); /** * ResourceLimitsResponse contains the maximum sandbox resource allocation. @@ -1503,7 +1804,43 @@ export type ResourceLimitsResponse = Message<"netclode.v1.ResourceLimitsResponse * Use `create(ResourceLimitsResponseSchema)` to create a new message. */ export const ResourceLimitsResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_netclode_v1_client, 44); + messageDesc(file_netclode_v1_client, 53); + +/** + * @generated from enum netclode.v1.CodexAuthState + */ +export enum CodexAuthState { + /** + * @generated from enum value: CODEX_AUTH_STATE_UNSPECIFIED = 0; + */ + UNSPECIFIED = 0, + + /** + * @generated from enum value: CODEX_AUTH_STATE_UNAUTHENTICATED = 1; + */ + UNAUTHENTICATED = 1, + + /** + * @generated from enum value: CODEX_AUTH_STATE_PENDING = 2; + */ + PENDING = 2, + + /** + * @generated from enum value: CODEX_AUTH_STATE_READY = 3; + */ + READY = 3, + + /** + * @generated from enum value: CODEX_AUTH_STATE_ERROR = 4; + */ + ERROR = 4, +} + +/** + * Describes the enum netclode.v1.CodexAuthState. + */ +export const CodexAuthStateSchema: GenEnum = /*@__PURE__*/ + enumDesc(file_netclode_v1_client, 0); /** * ClientService handles communication between clients and the control plane. diff --git a/services/agent/gen/netclode/v1/common_pb.ts b/services/agent/gen/netclode/v1/common_pb.ts index 47033020..8b9c63be 100644 --- a/services/agent/gen/netclode/v1/common_pb.ts +++ b/services/agent/gen/netclode/v1/common_pb.ts @@ -14,7 +14,7 @@ import type { Message } from "@bufbuild/protobuf"; * Describes the file netclode/v1/common.proto. */ export const file_netclode_v1_common: GenFile = /*@__PURE__*/ - fileDesc("ChhuZXRjbG9kZS92MS9jb21tb24ucHJvdG8SC25ldGNsb2RlLnYxIqwDCgdTZXNzaW9uEgoKAmlkGAEgASgJEgwKBG5hbWUYAiABKAkSKgoGc3RhdHVzGAMgASgOMhoubmV0Y2xvZGUudjEuU2Vzc2lvblN0YXR1cxINCgVyZXBvcxgEIAMoCRIxCgtyZXBvX2FjY2VzcxgFIAEoDjIXLm5ldGNsb2RlLnYxLlJlcG9BY2Nlc3NIAIgBARIuCgpjcmVhdGVkX2F0GAYgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcBIyCg5sYXN0X2FjdGl2ZV9hdBgHIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASKwoIc2RrX3R5cGUYCCABKA4yFC5uZXRjbG9kZS52MS5TZGtUeXBlSAGIAQESEgoFbW9kZWwYCSABKAlIAogBARI5Cg9jb3BpbG90X2JhY2tlbmQYCiABKA4yGy5uZXRjbG9kZS52MS5Db3BpbG90QmFja2VuZEgDiAEBQg4KDF9yZXBvX2FjY2Vzc0ILCglfc2RrX3R5cGVCCAoGX21vZGVsQhIKEF9jb3BpbG90X2JhY2tlbmQilQEKDlNlc3Npb25TdW1tYXJ5EiUKB3Nlc3Npb24YASABKAsyFC5uZXRjbG9kZS52MS5TZXNzaW9uEhoKDW1lc3NhZ2VfY291bnQYAiABKAVIAIgBARIbCg5sYXN0X3N0cmVhbV9pZBgDIAEoCUgBiAEBQhAKDl9tZXNzYWdlX2NvdW50QhEKD19sYXN0X3N0cmVhbV9pZCL0BgoNU2Vzc2lvbkNvbmZpZxISCgpzZXNzaW9uX2lkGAEgASgJEhUKDXdvcmtzcGFjZV9kaXIYAiABKAkSGQoMZ2l0aHViX3Rva2VuGAMgASgJSACIAQESDQoFcmVwb3MYBCADKAkSMQoLcmVwb19hY2Nlc3MYBSABKA4yFy5uZXRjbG9kZS52MS5SZXBvQWNjZXNzSAGIAQESGQoRY29udHJvbF9wbGFuZV91cmwYBiABKAkSKwoIc2RrX3R5cGUYByABKA4yFC5uZXRjbG9kZS52MS5TZGtUeXBlSAKIAQESEgoFbW9kZWwYCCABKAlIA4gBARI5Cg9jb3BpbG90X2JhY2tlbmQYCSABKA4yGy5uZXRjbG9kZS52MS5Db3BpbG90QmFja2VuZEgEiAEBEiEKFGdpdGh1Yl9jb3BpbG90X3Rva2VuGAogASgJSAWIAQESHwoSY29kZXhfYWNjZXNzX3Rva2VuGAsgASgJSAaIAQESGwoOY29kZXhfaWRfdG9rZW4YDCABKAlIB4gBARIbCg5vcGVuYWlfYXBpX2tleRgNIAEoCUgIiAEBEiAKE2NvZGV4X3JlZnJlc2hfdG9rZW4YDiABKAlICYgBARIdChByZWFzb25pbmdfZWZmb3J0GA8gASgJSAqIAQESHAoPbWlzdHJhbF9hcGlfa2V5GBAgASgJSAuIAQESFwoKb2xsYW1hX3VybBgRIAEoCUgMiAEBEh0KEG9wZW5jb2RlX2FwaV9rZXkYEiABKAlIDYgBARIYCgt6YWlfYXBpX2tleRgTIAEoCUgOiAEBQg8KDV9naXRodWJfdG9rZW5CDgoMX3JlcG9fYWNjZXNzQgsKCV9zZGtfdHlwZUIICgZfbW9kZWxCEgoQX2NvcGlsb3RfYmFja2VuZEIXChVfZ2l0aHViX2NvcGlsb3RfdG9rZW5CFQoTX2NvZGV4X2FjY2Vzc190b2tlbkIRCg9fY29kZXhfaWRfdG9rZW5CEQoPX29wZW5haV9hcGlfa2V5QhYKFF9jb2RleF9yZWZyZXNoX3Rva2VuQhMKEV9yZWFzb25pbmdfZWZmb3J0QhIKEF9taXN0cmFsX2FwaV9rZXlCDQoLX29sbGFtYV91cmxCEwoRX29wZW5jb2RlX2FwaV9rZXlCDgoMX3phaV9hcGlfa2V5IpsCCgtTdHJlYW1FbnRyeRIKCgJpZBgBIAEoCRItCgl0aW1lc3RhbXAYAiABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEg8KB3BhcnRpYWwYAyABKAgSKAoFZXZlbnQYBCABKAsyFy5uZXRjbG9kZS52MS5BZ2VudEV2ZW50SAASNgoPdGVybWluYWxfb3V0cHV0GAUgASgLMhsubmV0Y2xvZGUudjEuVGVybWluYWxPdXRwdXRIABIuCg5zZXNzaW9uX3VwZGF0ZRgGIAEoCzIULm5ldGNsb2RlLnYxLlNlc3Npb25IABIjCgVlcnJvchgHIAEoCzISLm5ldGNsb2RlLnYxLkVycm9ySABCCQoHcGF5bG9hZCIeCg5UZXJtaW5hbE91dHB1dBIMCgRkYXRhGAEgASgJIrABCgVFcnJvchIMCgRjb2RlGAEgASgJEg8KB21lc3NhZ2UYAiABKAkSFwoKc2Vzc2lvbl9pZBgDIAEoCUgAiAEBEjAKB2RldGFpbHMYBCADKAsyHy5uZXRjbG9kZS52MS5FcnJvci5EZXRhaWxzRW50cnkaLgoMRGV0YWlsc0VudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAFCDQoLX3Nlc3Npb25faWQi8gIKD0luUHJvZ3Jlc3NTdGF0ZRI8CghtZXNzYWdlcxgBIAMoCzIqLm5ldGNsb2RlLnYxLkluUHJvZ3Jlc3NTdGF0ZS5NZXNzYWdlc0VudHJ5EjwKCHRoaW5raW5nGAIgAygLMioubmV0Y2xvZGUudjEuSW5Qcm9ncmVzc1N0YXRlLlRoaW5raW5nRW50cnkSNgoFdG9vbHMYAyADKAsyJy5uZXRjbG9kZS52MS5JblByb2dyZXNzU3RhdGUuVG9vbHNFbnRyeRovCg1NZXNzYWdlc0VudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEaLwoNVGhpbmtpbmdFbnRyeRILCgNrZXkYASABKAkSDQoFdmFsdWUYAiABKAk6AjgBGkkKClRvb2xzRW50cnkSCwoDa2V5GAEgASgJEioKBXZhbHVlGAIgASgLMhsubmV0Y2xvZGUudjEuSW5Qcm9ncmVzc1Rvb2w6AjgBIj0KDkluUHJvZ3Jlc3NUb29sEgwKBHRvb2wYASABKAkSDQoFaW5wdXQYAiABKAkSDgoGb3V0cHV0GAMgASgJIrsBCghTbmFwc2hvdBIKCgJpZBgBIAEoCRISCgpzZXNzaW9uX2lkGAIgASgJEgwKBG5hbWUYAyABKAkSLgoKY3JlYXRlZF9hdBgEIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASEgoKc2l6ZV9ieXRlcxgFIAEoAxITCgt0dXJuX251bWJlchgGIAEoBRIVCg1tZXNzYWdlX2NvdW50GAcgASgFEhEKCXN0cmVhbV9pZBgIIAEoCSJoCgpHaXRIdWJSZXBvEgwKBG5hbWUYASABKAkSEQoJZnVsbF9uYW1lGAIgASgJEg8KB3ByaXZhdGUYAyABKAgSGAoLZGVzY3JpcHRpb24YBCABKAlIAIgBAUIOCgxfZGVzY3JpcHRpb24ivwEKDUdpdEZpbGVDaGFuZ2USDAoEcGF0aBgBIAEoCRIqCgZzdGF0dXMYAiABKA4yGi5uZXRjbG9kZS52MS5HaXRGaWxlU3RhdHVzEg4KBnN0YWdlZBgDIAEoCBIYCgtsaW5lc19hZGRlZBgEIAEoBUgAiAEBEhoKDWxpbmVzX3JlbW92ZWQYBSABKAVIAYgBARIMCgRyZXBvGAYgASgJQg4KDF9saW5lc19hZGRlZEIQCg5fbGluZXNfcmVtb3ZlZCKbAgoJTW9kZWxJbmZvEgoKAmlkGAEgASgJEgwKBG5hbWUYAiABKAkSFQoIcHJvdmlkZXIYAyABKAlIAIgBARIfChJiaWxsaW5nX211bHRpcGxpZXIYBCABKAFIAYgBARIUCgxjYXBhYmlsaXRpZXMYBSADKAkSHQoQcmVhc29uaW5nX2VmZm9ydBgGIAEoCUgCiAEBEhcKCmRvd25sb2FkZWQYByABKAhIA4gBARIXCgpzaXplX2J5dGVzGAggASgDSASIAQFCCwoJX3Byb3ZpZGVyQhUKE19iaWxsaW5nX211bHRpcGxpZXJCEwoRX3JlYXNvbmluZ19lZmZvcnRCDQoLX2Rvd25sb2FkZWRCDQoLX3NpemVfYnl0ZXMicQoRQ29waWxvdEF1dGhTdGF0dXMSGAoQaXNfYXV0aGVudGljYXRlZBgBIAEoCBIWCglhdXRoX3R5cGUYAiABKAlIAIgBARISCgVsb2dpbhgDIAEoCUgBiAEBQgwKCl9hdXRoX3R5cGVCCAoGX2xvZ2luImkKE0NvcGlsb3RQcmVtaXVtUXVvdGESDAoEdXNlZBgBIAEoBRINCgVsaW1pdBgCIAEoBRIRCglyZW1haW5pbmcYAyABKAUSFQoIcmVzZXRfYXQYBCABKAlIAIgBAUILCglfcmVzZXRfYXQiNAoQU2FuZGJveFJlc291cmNlcxINCgV2Y3B1cxgBIAEoBRIRCgltZW1vcnlfbWIYAiABKAUqVgoKUmVwb0FjY2VzcxIbChdSRVBPX0FDQ0VTU19VTlNQRUNJRklFRBAAEhQKEFJFUE9fQUNDRVNTX1JFQUQQARIVChFSRVBPX0FDQ0VTU19XUklURRACKnkKB1Nka1R5cGUSGAoUU0RLX1RZUEVfVU5TUEVDSUZJRUQQABITCg9TREtfVFlQRV9DTEFVREUQARIVChFTREtfVFlQRV9PUEVOQ09ERRACEhQKEFNES19UWVBFX0NPUElMT1QQAxISCg5TREtfVFlQRV9DT0RFWBAEKmwKDkNvcGlsb3RCYWNrZW5kEh8KG0NPUElMT1RfQkFDS0VORF9VTlNQRUNJRklFRBAAEhoKFkNPUElMT1RfQkFDS0VORF9HSVRIVUIQARIdChlDT1BJTE9UX0JBQ0tFTkRfQU5USFJPUElDEAIq9AEKDVNlc3Npb25TdGF0dXMSHgoaU0VTU0lPTl9TVEFUVVNfVU5TUEVDSUZJRUQQABIbChdTRVNTSU9OX1NUQVRVU19DUkVBVElORxABEhsKF1NFU1NJT05fU1RBVFVTX1JFU1VNSU5HEAISGAoUU0VTU0lPTl9TVEFUVVNfUkVBRFkQAxIaChZTRVNTSU9OX1NUQVRVU19SVU5OSU5HEAQSGQoVU0VTU0lPTl9TVEFUVVNfUEFVU0VEEAUSGAoUU0VTU0lPTl9TVEFUVVNfRVJST1IQBhIeChpTRVNTSU9OX1NUQVRVU19JTlRFUlJVUFRFRBAHKpkCCg1HaXRGaWxlU3RhdHVzEh8KG0dJVF9GSUxFX1NUQVRVU19VTlNQRUNJRklFRBAAEhwKGEdJVF9GSUxFX1NUQVRVU19NT0RJRklFRBABEhkKFUdJVF9GSUxFX1NUQVRVU19BRERFRBACEhsKF0dJVF9GSUxFX1NUQVRVU19ERUxFVEVEEAMSGwoXR0lUX0ZJTEVfU1RBVFVTX1JFTkFNRUQQBBIdChlHSVRfRklMRV9TVEFUVVNfVU5UUkFDS0VEEAUSGgoWR0lUX0ZJTEVfU1RBVFVTX0NPUElFRBAGEhsKF0dJVF9GSUxFX1NUQVRVU19JR05PUkVEEAcSHAoYR0lUX0ZJTEVfU1RBVFVTX1VOTUVSR0VEEAhCvAEKD2NvbS5uZXRjbG9kZS52MUILQ29tbW9uUHJvdG9QAVpPZ2l0aHViLmNvbS9hbmdyaXN0YW4vbmV0Y2xvZGUvc2VydmljZXMvY29udHJvbC1wbGFuZS9nZW4vbmV0Y2xvZGUvdjE7bmV0Y2xvZGV2MaICA05YWKoCC05ldGNsb2RlLlYxygILTmV0Y2xvZGVcVjHiAhdOZXRjbG9kZVxWMVxHUEJNZXRhZGF0YeoCDE5ldGNsb2RlOjpWMWIGcHJvdG8z", [file_google_protobuf_timestamp, file_netclode_v1_events]); + fileDesc("ChhuZXRjbG9kZS92MS9jb21tb24ucHJvdG8SC25ldGNsb2RlLnYxIqwDCgdTZXNzaW9uEgoKAmlkGAEgASgJEgwKBG5hbWUYAiABKAkSKgoGc3RhdHVzGAMgASgOMhoubmV0Y2xvZGUudjEuU2Vzc2lvblN0YXR1cxINCgVyZXBvcxgEIAMoCRIxCgtyZXBvX2FjY2VzcxgFIAEoDjIXLm5ldGNsb2RlLnYxLlJlcG9BY2Nlc3NIAIgBARIuCgpjcmVhdGVkX2F0GAYgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcBIyCg5sYXN0X2FjdGl2ZV9hdBgHIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASKwoIc2RrX3R5cGUYCCABKA4yFC5uZXRjbG9kZS52MS5TZGtUeXBlSAGIAQESEgoFbW9kZWwYCSABKAlIAogBARI5Cg9jb3BpbG90X2JhY2tlbmQYCiABKA4yGy5uZXRjbG9kZS52MS5Db3BpbG90QmFja2VuZEgDiAEBQg4KDF9yZXBvX2FjY2Vzc0ILCglfc2RrX3R5cGVCCAoGX21vZGVsQhIKEF9jb3BpbG90X2JhY2tlbmQilQEKDlNlc3Npb25TdW1tYXJ5EiUKB3Nlc3Npb24YASABKAsyFC5uZXRjbG9kZS52MS5TZXNzaW9uEhoKDW1lc3NhZ2VfY291bnQYAiABKAVIAIgBARIbCg5sYXN0X3N0cmVhbV9pZBgDIAEoCUgBiAEBQhAKDl9tZXNzYWdlX2NvdW50QhEKD19sYXN0X3N0cmVhbV9pZCK6BgoNU2Vzc2lvbkNvbmZpZxISCgpzZXNzaW9uX2lkGAEgASgJEhUKDXdvcmtzcGFjZV9kaXIYAiABKAkSGQoMZ2l0aHViX3Rva2VuGAMgASgJSACIAQESDQoFcmVwb3MYBCADKAkSMQoLcmVwb19hY2Nlc3MYBSABKA4yFy5uZXRjbG9kZS52MS5SZXBvQWNjZXNzSAGIAQESGQoRY29udHJvbF9wbGFuZV91cmwYBiABKAkSKwoIc2RrX3R5cGUYByABKA4yFC5uZXRjbG9kZS52MS5TZGtUeXBlSAKIAQESEgoFbW9kZWwYCCABKAlIA4gBARI5Cg9jb3BpbG90X2JhY2tlbmQYCSABKA4yGy5uZXRjbG9kZS52MS5Db3BpbG90QmFja2VuZEgEiAEBEiEKFGdpdGh1Yl9jb3BpbG90X3Rva2VuGAogASgJSAWIAQESHwoSY29kZXhfYWNjZXNzX3Rva2VuGAsgASgJSAaIAQESGwoOY29kZXhfaWRfdG9rZW4YDCABKAlIB4gBARIbCg5vcGVuYWlfYXBpX2tleRgNIAEoCUgIiAEBEh0KEHJlYXNvbmluZ19lZmZvcnQYDyABKAlICYgBARIcCg9taXN0cmFsX2FwaV9rZXkYECABKAlICogBARIXCgpvbGxhbWFfdXJsGBEgASgJSAuIAQESHQoQb3BlbmNvZGVfYXBpX2tleRgSIAEoCUgMiAEBEhgKC3phaV9hcGlfa2V5GBMgASgJSA2IAQFCDwoNX2dpdGh1Yl90b2tlbkIOCgxfcmVwb19hY2Nlc3NCCwoJX3Nka190eXBlQggKBl9tb2RlbEISChBfY29waWxvdF9iYWNrZW5kQhcKFV9naXRodWJfY29waWxvdF90b2tlbkIVChNfY29kZXhfYWNjZXNzX3Rva2VuQhEKD19jb2RleF9pZF90b2tlbkIRCg9fb3BlbmFpX2FwaV9rZXlCEwoRX3JlYXNvbmluZ19lZmZvcnRCEgoQX21pc3RyYWxfYXBpX2tleUINCgtfb2xsYW1hX3VybEITChFfb3BlbmNvZGVfYXBpX2tleUIOCgxfemFpX2FwaV9rZXkimwIKC1N0cmVhbUVudHJ5EgoKAmlkGAEgASgJEi0KCXRpbWVzdGFtcBgCIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASDwoHcGFydGlhbBgDIAEoCBIoCgVldmVudBgEIAEoCzIXLm5ldGNsb2RlLnYxLkFnZW50RXZlbnRIABI2Cg90ZXJtaW5hbF9vdXRwdXQYBSABKAsyGy5uZXRjbG9kZS52MS5UZXJtaW5hbE91dHB1dEgAEi4KDnNlc3Npb25fdXBkYXRlGAYgASgLMhQubmV0Y2xvZGUudjEuU2Vzc2lvbkgAEiMKBWVycm9yGAcgASgLMhIubmV0Y2xvZGUudjEuRXJyb3JIAEIJCgdwYXlsb2FkIh4KDlRlcm1pbmFsT3V0cHV0EgwKBGRhdGEYASABKAkisAEKBUVycm9yEgwKBGNvZGUYASABKAkSDwoHbWVzc2FnZRgCIAEoCRIXCgpzZXNzaW9uX2lkGAMgASgJSACIAQESMAoHZGV0YWlscxgEIAMoCzIfLm5ldGNsb2RlLnYxLkVycm9yLkRldGFpbHNFbnRyeRouCgxEZXRhaWxzRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgJOgI4AUINCgtfc2Vzc2lvbl9pZCLyAgoPSW5Qcm9ncmVzc1N0YXRlEjwKCG1lc3NhZ2VzGAEgAygLMioubmV0Y2xvZGUudjEuSW5Qcm9ncmVzc1N0YXRlLk1lc3NhZ2VzRW50cnkSPAoIdGhpbmtpbmcYAiADKAsyKi5uZXRjbG9kZS52MS5JblByb2dyZXNzU3RhdGUuVGhpbmtpbmdFbnRyeRI2CgV0b29scxgDIAMoCzInLm5ldGNsb2RlLnYxLkluUHJvZ3Jlc3NTdGF0ZS5Ub29sc0VudHJ5Gi8KDU1lc3NhZ2VzRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgJOgI4ARovCg1UaGlua2luZ0VudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEaSQoKVG9vbHNFbnRyeRILCgNrZXkYASABKAkSKgoFdmFsdWUYAiABKAsyGy5uZXRjbG9kZS52MS5JblByb2dyZXNzVG9vbDoCOAEiPQoOSW5Qcm9ncmVzc1Rvb2wSDAoEdG9vbBgBIAEoCRINCgVpbnB1dBgCIAEoCRIOCgZvdXRwdXQYAyABKAkiuwEKCFNuYXBzaG90EgoKAmlkGAEgASgJEhIKCnNlc3Npb25faWQYAiABKAkSDAoEbmFtZRgDIAEoCRIuCgpjcmVhdGVkX2F0GAQgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcBISCgpzaXplX2J5dGVzGAUgASgDEhMKC3R1cm5fbnVtYmVyGAYgASgFEhUKDW1lc3NhZ2VfY291bnQYByABKAUSEQoJc3RyZWFtX2lkGAggASgJImgKCkdpdEh1YlJlcG8SDAoEbmFtZRgBIAEoCRIRCglmdWxsX25hbWUYAiABKAkSDwoHcHJpdmF0ZRgDIAEoCBIYCgtkZXNjcmlwdGlvbhgEIAEoCUgAiAEBQg4KDF9kZXNjcmlwdGlvbiK/AQoNR2l0RmlsZUNoYW5nZRIMCgRwYXRoGAEgASgJEioKBnN0YXR1cxgCIAEoDjIaLm5ldGNsb2RlLnYxLkdpdEZpbGVTdGF0dXMSDgoGc3RhZ2VkGAMgASgIEhgKC2xpbmVzX2FkZGVkGAQgASgFSACIAQESGgoNbGluZXNfcmVtb3ZlZBgFIAEoBUgBiAEBEgwKBHJlcG8YBiABKAlCDgoMX2xpbmVzX2FkZGVkQhAKDl9saW5lc19yZW1vdmVkIpsCCglNb2RlbEluZm8SCgoCaWQYASABKAkSDAoEbmFtZRgCIAEoCRIVCghwcm92aWRlchgDIAEoCUgAiAEBEh8KEmJpbGxpbmdfbXVsdGlwbGllchgEIAEoAUgBiAEBEhQKDGNhcGFiaWxpdGllcxgFIAMoCRIdChByZWFzb25pbmdfZWZmb3J0GAYgASgJSAKIAQESFwoKZG93bmxvYWRlZBgHIAEoCEgDiAEBEhcKCnNpemVfYnl0ZXMYCCABKANIBIgBAUILCglfcHJvdmlkZXJCFQoTX2JpbGxpbmdfbXVsdGlwbGllckITChFfcmVhc29uaW5nX2VmZm9ydEINCgtfZG93bmxvYWRlZEINCgtfc2l6ZV9ieXRlcyJxChFDb3BpbG90QXV0aFN0YXR1cxIYChBpc19hdXRoZW50aWNhdGVkGAEgASgIEhYKCWF1dGhfdHlwZRgCIAEoCUgAiAEBEhIKBWxvZ2luGAMgASgJSAGIAQFCDAoKX2F1dGhfdHlwZUIICgZfbG9naW4iaQoTQ29waWxvdFByZW1pdW1RdW90YRIMCgR1c2VkGAEgASgFEg0KBWxpbWl0GAIgASgFEhEKCXJlbWFpbmluZxgDIAEoBRIVCghyZXNldF9hdBgEIAEoCUgAiAEBQgsKCV9yZXNldF9hdCI0ChBTYW5kYm94UmVzb3VyY2VzEg0KBXZjcHVzGAEgASgFEhEKCW1lbW9yeV9tYhgCIAEoBSpWCgpSZXBvQWNjZXNzEhsKF1JFUE9fQUNDRVNTX1VOU1BFQ0lGSUVEEAASFAoQUkVQT19BQ0NFU1NfUkVBRBABEhUKEVJFUE9fQUNDRVNTX1dSSVRFEAIqeQoHU2RrVHlwZRIYChRTREtfVFlQRV9VTlNQRUNJRklFRBAAEhMKD1NES19UWVBFX0NMQVVERRABEhUKEVNES19UWVBFX09QRU5DT0RFEAISFAoQU0RLX1RZUEVfQ09QSUxPVBADEhIKDlNES19UWVBFX0NPREVYEAQqbAoOQ29waWxvdEJhY2tlbmQSHwobQ09QSUxPVF9CQUNLRU5EX1VOU1BFQ0lGSUVEEAASGgoWQ09QSUxPVF9CQUNLRU5EX0dJVEhVQhABEh0KGUNPUElMT1RfQkFDS0VORF9BTlRIUk9QSUMQAir0AQoNU2Vzc2lvblN0YXR1cxIeChpTRVNTSU9OX1NUQVRVU19VTlNQRUNJRklFRBAAEhsKF1NFU1NJT05fU1RBVFVTX0NSRUFUSU5HEAESGwoXU0VTU0lPTl9TVEFUVVNfUkVTVU1JTkcQAhIYChRTRVNTSU9OX1NUQVRVU19SRUFEWRADEhoKFlNFU1NJT05fU1RBVFVTX1JVTk5JTkcQBBIZChVTRVNTSU9OX1NUQVRVU19QQVVTRUQQBRIYChRTRVNTSU9OX1NUQVRVU19FUlJPUhAGEh4KGlNFU1NJT05fU1RBVFVTX0lOVEVSUlVQVEVEEAcqmQIKDUdpdEZpbGVTdGF0dXMSHwobR0lUX0ZJTEVfU1RBVFVTX1VOU1BFQ0lGSUVEEAASHAoYR0lUX0ZJTEVfU1RBVFVTX01PRElGSUVEEAESGQoVR0lUX0ZJTEVfU1RBVFVTX0FEREVEEAISGwoXR0lUX0ZJTEVfU1RBVFVTX0RFTEVURUQQAxIbChdHSVRfRklMRV9TVEFUVVNfUkVOQU1FRBAEEh0KGUdJVF9GSUxFX1NUQVRVU19VTlRSQUNLRUQQBRIaChZHSVRfRklMRV9TVEFUVVNfQ09QSUVEEAYSGwoXR0lUX0ZJTEVfU1RBVFVTX0lHTk9SRUQQBxIcChhHSVRfRklMRV9TVEFUVVNfVU5NRVJHRUQQCEK8AQoPY29tLm5ldGNsb2RlLnYxQgtDb21tb25Qcm90b1ABWk9naXRodWIuY29tL2FuZ3Jpc3Rhbi9uZXRjbG9kZS9zZXJ2aWNlcy9jb250cm9sLXBsYW5lL2dlbi9uZXRjbG9kZS92MTtuZXRjbG9kZXYxogIDTlhYqgILTmV0Y2xvZGUuVjHKAgtOZXRjbG9kZVxWMeICF05ldGNsb2RlXFYxXEdQQk1ldGFkYXRh6gIMTmV0Y2xvZGU6OlYxYgZwcm90bzM", [file_google_protobuf_timestamp, file_netclode_v1_events]); /** * Session represents a coding session with an AI agent. @@ -182,11 +182,6 @@ export type SessionConfig = Message<"netclode.v1.SessionConfig"> & { */ openaiApiKey?: string; - /** - * @generated from field: optional string codex_refresh_token = 14; - */ - codexRefreshToken?: string; - /** * @generated from field: optional string reasoning_effort = 15; */ diff --git a/services/agent/gen/netclode/v1/events_pb.ts b/services/agent/gen/netclode/v1/events_pb.ts index aaa4c51c..2946be6b 100644 --- a/services/agent/gen/netclode/v1/events_pb.ts +++ b/services/agent/gen/netclode/v1/events_pb.ts @@ -11,7 +11,7 @@ import type { JsonObject, Message } from "@bufbuild/protobuf"; * Describes the file netclode/v1/events.proto. */ export const file_netclode_v1_events: GenFile = /*@__PURE__*/ - fileDesc("ChhuZXRjbG9kZS92MS9ldmVudHMucHJvdG8SC25ldGNsb2RlLnYxIvwDCgpBZ2VudEV2ZW50EikKBGtpbmQYASABKA4yGy5uZXRjbG9kZS52MS5BZ2VudEV2ZW50S2luZBIWCg5jb3JyZWxhdGlvbl9pZBgCIAEoCRIuCgdtZXNzYWdlGAMgASgLMhsubmV0Y2xvZGUudjEuTWVzc2FnZVBheWxvYWRIABIwCgh0aGlua2luZxgEIAEoCzIcLm5ldGNsb2RlLnYxLlRoaW5raW5nUGF5bG9hZEgAEjMKCnRvb2xfc3RhcnQYBSABKAsyHS5uZXRjbG9kZS52MS5Ub29sU3RhcnRQYXlsb2FkSAASMwoKdG9vbF9pbnB1dBgGIAEoCzIdLm5ldGNsb2RlLnYxLlRvb2xJbnB1dFBheWxvYWRIABI1Cgt0b29sX291dHB1dBgHIAEoCzIeLm5ldGNsb2RlLnYxLlRvb2xPdXRwdXRQYXlsb2FkSAASLwoIdG9vbF9lbmQYCCABKAsyGy5uZXRjbG9kZS52MS5Ub29sRW5kUGF5bG9hZEgAEjcKDHBvcnRfZXhwb3NlZBgJIAEoCzIfLm5ldGNsb2RlLnYxLlBvcnRFeHBvc2VkUGF5bG9hZEgAEjMKCnJlcG9fY2xvbmUYCiABKAsyHS5uZXRjbG9kZS52MS5SZXBvQ2xvbmVQYXlsb2FkSABCCQoHcGF5bG9hZCJJCg5NZXNzYWdlUGF5bG9hZBImCgRyb2xlGAEgASgOMhgubmV0Y2xvZGUudjEuTWVzc2FnZVJvbGUSDwoHY29udGVudBgCIAEoCSIzCg9UaGlua2luZ1BheWxvYWQSDwoHY29udGVudBgBIAEoCRIPCgdwYXJ0aWFsGAIgASgIIlgKEFRvb2xTdGFydFBheWxvYWQSDAoEdG9vbBgBIAEoCRIfChJwYXJlbnRfdG9vbF91c2VfaWQYAiABKAlIAIgBAUIVChNfcGFyZW50X3Rvb2xfdXNlX2lkImcKEFRvb2xJbnB1dFBheWxvYWQSEgoFZGVsdGEYASABKAlIAIgBARIrCgVpbnB1dBgCIAEoCzIXLmdvb2dsZS5wcm90b2J1Zi5TdHJ1Y3RIAYgBAUIICgZfZGVsdGFCCAoGX2lucHV0IlEKEVRvb2xPdXRwdXRQYXlsb2FkEhIKBWRlbHRhGAEgASgJSACIAQESEwoGb3V0cHV0GAIgASgJSAGIAQFCCAoGX2RlbHRhQgkKB19vdXRwdXQiiQEKDlRvb2xFbmRQYXlsb2FkEg8KB3N1Y2Nlc3MYASABKAgSEgoFZXJyb3IYAiABKAlIAIgBARIYCgtkdXJhdGlvbl9tcxgDIAEoA0gBiAEBEhMKBnJlc3VsdBgEIAEoCUgCiAEBQggKBl9lcnJvckIOCgxfZHVyYXRpb25fbXNCCQoHX3Jlc3VsdCJuChJQb3J0RXhwb3NlZFBheWxvYWQSDAoEcG9ydBgBIAEoBRIUCgdwcm9jZXNzGAIgASgJSACIAQESGAoLcHJldmlld191cmwYAyABKAlIAYgBAUIKCghfcHJvY2Vzc0IOCgxfcHJldmlld191cmwiXQoQUmVwb0Nsb25lUGF5bG9hZBIMCgRyZXBvGAEgASgJEioKBXN0YWdlGAIgASgOMhsubmV0Y2xvZGUudjEuUmVwb0Nsb25lU3RhZ2USDwoHbWVzc2FnZRgDIAEoCSqHAwoOQWdlbnRFdmVudEtpbmQSIAocQUdFTlRfRVZFTlRfS0lORF9VTlNQRUNJRklFRBAAEhwKGEFHRU5UX0VWRU5UX0tJTkRfTUVTU0FHRRABEh0KGUFHRU5UX0VWRU5UX0tJTkRfVEhJTktJTkcQAhIfChtBR0VOVF9FVkVOVF9LSU5EX1RPT0xfU1RBUlQQAxIfChtBR0VOVF9FVkVOVF9LSU5EX1RPT0xfSU5QVVQQBBIgChxBR0VOVF9FVkVOVF9LSU5EX1RPT0xfT1VUUFVUEAUSHQoZQUdFTlRfRVZFTlRfS0lORF9UT09MX0VORBAGEiEKHUFHRU5UX0VWRU5UX0tJTkRfUE9SVF9FWFBPU0VEEAcSHwobQUdFTlRfRVZFTlRfS0lORF9SRVBPX0NMT05FEAgSJwojQUdFTlRfRVZFTlRfS0lORF9BR0VOVF9ESVNDT05ORUNURUQQCRImCiJBR0VOVF9FVkVOVF9LSU5EX0FHRU5UX1JFQ09OTkVDVEVEEAoqXgoLTWVzc2FnZVJvbGUSHAoYTUVTU0FHRV9ST0xFX1VOU1BFQ0lGSUVEEAASFQoRTUVTU0FHRV9ST0xFX1VTRVIQARIaChZNRVNTQUdFX1JPTEVfQVNTSVNUQU5UEAIqpgEKDlJlcG9DbG9uZVN0YWdlEiAKHFJFUE9fQ0xPTkVfU1RBR0VfVU5TUEVDSUZJRUQQABIdChlSRVBPX0NMT05FX1NUQUdFX1NUQVJUSU5HEAESHAoYUkVQT19DTE9ORV9TVEFHRV9DTE9OSU5HEAISGQoVUkVQT19DTE9ORV9TVEFHRV9ET05FEAMSGgoWUkVQT19DTE9ORV9TVEFHRV9FUlJPUhAEQrwBCg9jb20ubmV0Y2xvZGUudjFCC0V2ZW50c1Byb3RvUAFaT2dpdGh1Yi5jb20vYW5ncmlzdGFuL25ldGNsb2RlL3NlcnZpY2VzL2NvbnRyb2wtcGxhbmUvZ2VuL25ldGNsb2RlL3YxO25ldGNsb2RldjGiAgNOWFiqAgtOZXRjbG9kZS5WMcoCC05ldGNsb2RlXFYx4gIXTmV0Y2xvZGVcVjFcR1BCTWV0YWRhdGHqAgxOZXRjbG9kZTo6VjFiBnByb3RvMw", [file_google_protobuf_struct]); + fileDesc("ChhuZXRjbG9kZS92MS9ldmVudHMucHJvdG8SC25ldGNsb2RlLnYxIrkECgpBZ2VudEV2ZW50EikKBGtpbmQYASABKA4yGy5uZXRjbG9kZS52MS5BZ2VudEV2ZW50S2luZBIWCg5jb3JyZWxhdGlvbl9pZBgCIAEoCRIuCgdtZXNzYWdlGAMgASgLMhsubmV0Y2xvZGUudjEuTWVzc2FnZVBheWxvYWRIABIwCgh0aGlua2luZxgEIAEoCzIcLm5ldGNsb2RlLnYxLlRoaW5raW5nUGF5bG9hZEgAEjMKCnRvb2xfc3RhcnQYBSABKAsyHS5uZXRjbG9kZS52MS5Ub29sU3RhcnRQYXlsb2FkSAASMwoKdG9vbF9pbnB1dBgGIAEoCzIdLm5ldGNsb2RlLnYxLlRvb2xJbnB1dFBheWxvYWRIABI1Cgt0b29sX291dHB1dBgHIAEoCzIeLm5ldGNsb2RlLnYxLlRvb2xPdXRwdXRQYXlsb2FkSAASLwoIdG9vbF9lbmQYCCABKAsyGy5uZXRjbG9kZS52MS5Ub29sRW5kUGF5bG9hZEgAEjcKDHBvcnRfZXhwb3NlZBgJIAEoCzIfLm5ldGNsb2RlLnYxLlBvcnRFeHBvc2VkUGF5bG9hZEgAEjMKCnJlcG9fY2xvbmUYCiABKAsyHS5uZXRjbG9kZS52MS5SZXBvQ2xvbmVQYXlsb2FkSAASOwoOcG9ydF91bmV4cG9zZWQYCyABKAsyIS5uZXRjbG9kZS52MS5Qb3J0VW5leHBvc2VkUGF5bG9hZEgAQgkKB3BheWxvYWQiSQoOTWVzc2FnZVBheWxvYWQSJgoEcm9sZRgBIAEoDjIYLm5ldGNsb2RlLnYxLk1lc3NhZ2VSb2xlEg8KB2NvbnRlbnQYAiABKAkiMwoPVGhpbmtpbmdQYXlsb2FkEg8KB2NvbnRlbnQYASABKAkSDwoHcGFydGlhbBgCIAEoCCJYChBUb29sU3RhcnRQYXlsb2FkEgwKBHRvb2wYASABKAkSHwoScGFyZW50X3Rvb2xfdXNlX2lkGAIgASgJSACIAQFCFQoTX3BhcmVudF90b29sX3VzZV9pZCJnChBUb29sSW5wdXRQYXlsb2FkEhIKBWRlbHRhGAEgASgJSACIAQESKwoFaW5wdXQYAiABKAsyFy5nb29nbGUucHJvdG9idWYuU3RydWN0SAGIAQFCCAoGX2RlbHRhQggKBl9pbnB1dCJRChFUb29sT3V0cHV0UGF5bG9hZBISCgVkZWx0YRgBIAEoCUgAiAEBEhMKBm91dHB1dBgCIAEoCUgBiAEBQggKBl9kZWx0YUIJCgdfb3V0cHV0IokBCg5Ub29sRW5kUGF5bG9hZBIPCgdzdWNjZXNzGAEgASgIEhIKBWVycm9yGAIgASgJSACIAQESGAoLZHVyYXRpb25fbXMYAyABKANIAYgBARITCgZyZXN1bHQYBCABKAlIAogBAUIICgZfZXJyb3JCDgoMX2R1cmF0aW9uX21zQgkKB19yZXN1bHQibgoSUG9ydEV4cG9zZWRQYXlsb2FkEgwKBHBvcnQYASABKAUSFAoHcHJvY2VzcxgCIAEoCUgAiAEBEhgKC3ByZXZpZXdfdXJsGAMgASgJSAGIAQFCCgoIX3Byb2Nlc3NCDgoMX3ByZXZpZXdfdXJsIiQKFFBvcnRVbmV4cG9zZWRQYXlsb2FkEgwKBHBvcnQYASABKAUiXQoQUmVwb0Nsb25lUGF5bG9hZBIMCgRyZXBvGAEgASgJEioKBXN0YWdlGAIgASgOMhsubmV0Y2xvZGUudjEuUmVwb0Nsb25lU3RhZ2USDwoHbWVzc2FnZRgDIAEoCSqsAwoOQWdlbnRFdmVudEtpbmQSIAocQUdFTlRfRVZFTlRfS0lORF9VTlNQRUNJRklFRBAAEhwKGEFHRU5UX0VWRU5UX0tJTkRfTUVTU0FHRRABEh0KGUFHRU5UX0VWRU5UX0tJTkRfVEhJTktJTkcQAhIfChtBR0VOVF9FVkVOVF9LSU5EX1RPT0xfU1RBUlQQAxIfChtBR0VOVF9FVkVOVF9LSU5EX1RPT0xfSU5QVVQQBBIgChxBR0VOVF9FVkVOVF9LSU5EX1RPT0xfT1VUUFVUEAUSHQoZQUdFTlRfRVZFTlRfS0lORF9UT09MX0VORBAGEiEKHUFHRU5UX0VWRU5UX0tJTkRfUE9SVF9FWFBPU0VEEAcSHwobQUdFTlRfRVZFTlRfS0lORF9SRVBPX0NMT05FEAgSJwojQUdFTlRfRVZFTlRfS0lORF9BR0VOVF9ESVNDT05ORUNURUQQCRImCiJBR0VOVF9FVkVOVF9LSU5EX0FHRU5UX1JFQ09OTkVDVEVEEAoSIwofQUdFTlRfRVZFTlRfS0lORF9QT1JUX1VORVhQT1NFRBALKl4KC01lc3NhZ2VSb2xlEhwKGE1FU1NBR0VfUk9MRV9VTlNQRUNJRklFRBAAEhUKEU1FU1NBR0VfUk9MRV9VU0VSEAESGgoWTUVTU0FHRV9ST0xFX0FTU0lTVEFOVBACKqYBCg5SZXBvQ2xvbmVTdGFnZRIgChxSRVBPX0NMT05FX1NUQUdFX1VOU1BFQ0lGSUVEEAASHQoZUkVQT19DTE9ORV9TVEFHRV9TVEFSVElORxABEhwKGFJFUE9fQ0xPTkVfU1RBR0VfQ0xPTklORxACEhkKFVJFUE9fQ0xPTkVfU1RBR0VfRE9ORRADEhoKFlJFUE9fQ0xPTkVfU1RBR0VfRVJST1IQBEK8AQoPY29tLm5ldGNsb2RlLnYxQgtFdmVudHNQcm90b1ABWk9naXRodWIuY29tL2FuZ3Jpc3Rhbi9uZXRjbG9kZS9zZXJ2aWNlcy9jb250cm9sLXBsYW5lL2dlbi9uZXRjbG9kZS92MTtuZXRjbG9kZXYxogIDTlhYqgILTmV0Y2xvZGUuVjHKAgtOZXRjbG9kZVxWMeICF05ldGNsb2RlXFYxXEdQQk1ldGFkYXRh6gIMTmV0Y2xvZGU6OlYxYgZwcm90bzM", [file_google_protobuf_struct]); /** * AgentEvent represents events emitted during agent execution. @@ -84,6 +84,12 @@ export type AgentEvent = Message<"netclode.v1.AgentEvent"> & { */ value: RepoClonePayload; case: "repoClone"; + } | { + /** + * @generated from field: netclode.v1.PortUnexposedPayload port_unexposed = 11; + */ + value: PortUnexposedPayload; + case: "portUnexposed"; } | { case: undefined; value?: undefined }; }; @@ -317,6 +323,27 @@ export type PortExposedPayload = Message<"netclode.v1.PortExposedPayload"> & { export const PortExposedPayloadSchema: GenMessage = /*@__PURE__*/ messageDesc(file_netclode_v1_events, 7); +/** + * PortUnexposedPayload contains data for port removal events. + * + * @generated from message netclode.v1.PortUnexposedPayload + */ +export type PortUnexposedPayload = Message<"netclode.v1.PortUnexposedPayload"> & { + /** + * The port number no longer exposed + * + * @generated from field: int32 port = 1; + */ + port: number; +}; + +/** + * Describes the message netclode.v1.PortUnexposedPayload. + * Use `create(PortUnexposedPayloadSchema)` to create a new message. + */ +export const PortUnexposedPayloadSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_netclode_v1_events, 8); + /** * RepoClonePayload contains data for repository clone progress events. * @@ -350,7 +377,7 @@ export type RepoClonePayload = Message<"netclode.v1.RepoClonePayload"> & { * Use `create(RepoClonePayloadSchema)` to create a new message. */ export const RepoClonePayloadSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_netclode_v1_events, 8); + messageDesc(file_netclode_v1_events, 9); /** * AgentEventKind identifies the type of event. @@ -438,6 +465,13 @@ export enum AgentEventKind { * @generated from enum value: AGENT_EVENT_KIND_AGENT_RECONNECTED = 10; */ AGENT_RECONNECTED = 10, + + /** + * Port exposure was removed + * + * @generated from enum value: AGENT_EVENT_KIND_PORT_UNEXPOSED = 11; + */ + PORT_UNEXPOSED = 11, } /** diff --git a/services/agent/package-lock.json b/services/agent/package-lock.json index 9134ceee..3ab46869 100644 --- a/services/agent/package-lock.json +++ b/services/agent/package-lock.json @@ -14,7 +14,7 @@ "@connectrpc/connect": "^2.1.1", "@connectrpc/connect-node": "^2.1.1", "@github/copilot-sdk": "^0.1.23", - "@openai/codex-sdk": "^0.93.0", + "@openai/codex-sdk": "^0.98.0", "node-pty": "^1.0.0", "undici": "^7.21.0" }, @@ -967,9 +967,9 @@ "license": "MIT" }, "node_modules/@openai/codex-sdk": { - "version": "0.93.0", - "resolved": "https://registry.npmjs.org/@openai/codex-sdk/-/codex-sdk-0.93.0.tgz", - "integrity": "sha512-9eHMhbXVIylI+lgh+Kput2DEakbYmJBTGHMXgzWwV58/NRrYVCKiAbWS462fHnlRxrJcF7Lmofa0V7uOZp7w7Q==", + "version": "0.98.0", + "resolved": "https://registry.npmjs.org/@openai/codex-sdk/-/codex-sdk-0.98.0.tgz", + "integrity": "sha512-TbPgrBpuSNMJyOXys0HNsh6UoP5VIHu1fVh2KDdACi5XyB0vuPtzBZC+qOsxHz7WXEQPFlomPLyxS6JnE5Okmg==", "license": "Apache-2.0", "engines": { "node": ">=18" diff --git a/services/agent/package.json b/services/agent/package.json index facf887d..f7127a1a 100644 --- a/services/agent/package.json +++ b/services/agent/package.json @@ -17,7 +17,7 @@ "@connectrpc/connect": "^2.1.1", "@connectrpc/connect-node": "^2.1.1", "@github/copilot-sdk": "^0.1.23", - "@openai/codex-sdk": "^0.93.0", + "@openai/codex-sdk": "^0.98.0", "node-pty": "^1.0.0", "undici": "^7.21.0" }, diff --git a/services/agent/src/connect-client.ts b/services/agent/src/connect-client.ts index ad96ed87..e7d960b1 100644 --- a/services/agent/src/connect-client.ts +++ b/services/agent/src/connect-client.ts @@ -51,6 +51,7 @@ import { getGitStatus, getGitDiff, configureGitCredentials, getRepoPath, getRepo // Import SDK abstraction layer import { createSDKAdapter, + shutdownAllAdapters, type SDKAdapter, type PromptEvent, type SdkType, @@ -429,15 +430,13 @@ export async function connectToControlPlane( setTerminalOutputCallback(null); connection = null; - // Shutdown SDK adapters - if (currentAdapter) { - try { - await currentAdapter.shutdown(); - } catch (err) { - console.error("[agent] Error shutting down SDK adapter:", err); - } - currentAdapter = null; + // Shutdown and clear cached SDK adapters so reconnect initializes fresh clients. + try { + await shutdownAllAdapters(); + } catch (err) { + console.error("[agent] Error shutting down SDK adapters:", err); } + currentAdapter = null; console.log("[agent] Disconnected from control plane"); } @@ -483,7 +482,6 @@ async function handleControlPlaneMessage( openaiApiKey: process.env.OPENAI_API_KEY || "", codexAccessToken: config.codexAccessToken, codexIdToken: config.codexIdToken, - codexRefreshToken: config.codexRefreshToken, reasoningEffort: config.reasoningEffort, mistralApiKey: process.env.MISTRAL_API_KEY || "", ollamaUrl: config.ollamaUrl, @@ -544,6 +542,10 @@ async function handleControlPlaneMessage( await handleUpdateGitCredentials(msg.message.value); break; + case "updateCodexAuth": + await handleUpdateCodexAuth(msg.message.value); + break; + case "sessionAssigned": // Warm pool mode: session was assigned to us await handleSessionAssigned(sessionId, msg.message.value, send); @@ -570,6 +572,32 @@ async function handleUpdateGitCredentials(credentials: { } } +/** + * Handle runtime Codex OAuth token updates from control-plane. + */ +async function handleUpdateCodexAuth(tokens: { + accessToken: string; + idToken: string; + expiresAt?: { seconds: bigint | string | number; nanos: number }; +}): Promise { + if (connection?.sessionConfig) { + connection.sessionConfig.codexAccessToken = tokens.accessToken; + connection.sessionConfig.codexIdToken = tokens.idToken; + } + + if (!currentAdapter || typeof currentAdapter.updateCodexAuth !== "function") { + console.warn("[agent] Received Codex OAuth update but Codex adapter is not active"); + return; + } + + let expiresAtDate: Date | undefined; + if (tokens.expiresAt) { + expiresAtDate = new Date(Number(tokens.expiresAt.seconds) * 1000 + Math.floor(tokens.expiresAt.nanos / 1_000_000)); + } + + await currentAdapter.updateCodexAuth(tokens.accessToken, tokens.idToken, expiresAtDate); +} + /** * Handle session assigned (warm pool mode) - initialize SDK with pushed config */ @@ -608,7 +636,6 @@ async function handleSessionAssigned( openaiApiKey: process.env.OPENAI_API_KEY || "", codexAccessToken: config.codexAccessToken, codexIdToken: config.codexIdToken, - codexRefreshToken: config.codexRefreshToken, reasoningEffort: config.reasoningEffort, mistralApiKey: process.env.MISTRAL_API_KEY || "", ollamaUrl: config.ollamaUrl, diff --git a/services/agent/src/sdk/codex/adapter.interrupt.test.ts b/services/agent/src/sdk/codex/adapter.interrupt.test.ts new file mode 100644 index 00000000..858113f1 --- /dev/null +++ b/services/agent/src/sdk/codex/adapter.interrupt.test.ts @@ -0,0 +1,92 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; +import type { SDKConfig } from "../types.js"; + +const runStreamedMock = vi.fn(); + +vi.mock("@openai/codex-sdk", () => { + class MockCodex { + startThread() { + return { + id: "thread-1", + runStreamed: runStreamedMock, + }; + } + + resumeThread() { + return { + id: "thread-1", + runStreamed: runStreamedMock, + }; + } + } + + return { Codex: MockCodex }; +}); + +import { CodexAdapter } from "./adapter.js"; + +function makeConfig(overrides: Partial = {}): SDKConfig { + return { + sdkType: "codex", + workspaceDir: "/tmp/workspace", + anthropicApiKey: "", + model: "gpt-5-codex:oauth:high", + codexAccessToken: "access-token", + codexIdToken: "eyJhbGciOiJub25lIn0.eyJodHRwczovL2FwaS5vcGVuYWkuY29tL2F1dGgiOnsiY2hhdGdwdF9hY2NvdW50X2lkIjoiYWNjdC0xIn19.", + ...overrides, + }; +} + +describe("CodexAdapter interrupt behavior", () => { + beforeEach(() => { + runStreamedMock.mockReset(); + process.env.CODEX_HOME = `/tmp/netclode-codex-test-${Date.now()}-${Math.random()}`; + }); + + it("passes AbortSignal to runStreamed and yields interrupt error on abort", async () => { + runStreamedMock.mockImplementation( + (_input: string, options?: { signal?: AbortSignal }) => + new Promise((_resolve, reject) => { + if (!options?.signal) { + reject(new Error("missing signal")); + return; + } + + const abort = () => { + const err = new Error("operation aborted"); + err.name = "AbortError"; + reject(err); + }; + + if (options.signal.aborted) { + abort(); + return; + } + + options.signal.addEventListener("abort", abort, { once: true }); + }) + ); + + const adapter = new CodexAdapter(); + await adapter.initialize(makeConfig()); + + const iterator = adapter.executePrompt("sess-1", "hello")[Symbol.asyncIterator](); + const firstEventPromise = iterator.next(); + + await vi.waitFor(() => { + expect(runStreamedMock).toHaveBeenCalledTimes(1); + }); + adapter.setInterruptSignal(); + + const firstEvent = await firstEventPromise; + + expect(runStreamedMock).toHaveBeenCalledTimes(1); + expect(runStreamedMock.mock.calls[0][1]?.signal).toBeInstanceOf(AbortSignal); + expect(firstEvent.done).toBe(false); + expect(firstEvent.value).toEqual({ + type: "error", + message: "Prompt interrupted", + retryable: true, + }); + }); +}); diff --git a/services/agent/src/sdk/codex/adapter.test.ts b/services/agent/src/sdk/codex/adapter.test.ts new file mode 100644 index 00000000..37c0e669 --- /dev/null +++ b/services/agent/src/sdk/codex/adapter.test.ts @@ -0,0 +1,57 @@ +import { describe, expect, it } from "vitest"; +import { resolveCodexAuthMode } from "./adapter.js"; +import type { SDKConfig } from "../types.js"; + +function makeConfig(overrides: Partial = {}): SDKConfig { + return { + sdkType: "codex", + workspaceDir: "/tmp/workspace", + anthropicApiKey: "", + ...overrides, + }; +} + +describe("resolveCodexAuthMode", () => { + it("prefers explicit oauth suffix", () => { + const mode = resolveCodexAuthMode( + makeConfig({ + model: "gpt-5-codex:oauth:high", + openaiApiKey: "sk-real", + }) + ); + expect(mode).toBe("oauth"); + }); + + it("prefers explicit api suffix", () => { + const mode = resolveCodexAuthMode( + makeConfig({ + model: "gpt-5-codex:api:medium", + codexAccessToken: "oauth-access", + codexIdToken: "oauth-id", + }) + ); + expect(mode).toBe("api"); + }); + + it("prefers oauth tokens when suffix is omitted", () => { + const mode = resolveCodexAuthMode( + makeConfig({ + model: "gpt-5-codex", + openaiApiKey: "sk-real", + codexAccessToken: "oauth-access", + codexIdToken: "oauth-id", + }) + ); + expect(mode).toBe("oauth"); + }); + + it("ignores Netclode placeholder api key", () => { + const mode = resolveCodexAuthMode( + makeConfig({ + model: "gpt-5-codex", + openaiApiKey: "NETCLODE_PLACEHOLDER_openai", + }) + ); + expect(mode).toBe("unknown"); + }); +}); diff --git a/services/agent/src/sdk/codex/adapter.ts b/services/agent/src/sdk/codex/adapter.ts index 26522651..fcf80b27 100644 --- a/services/agent/src/sdk/codex/adapter.ts +++ b/services/agent/src/sdk/codex/adapter.ts @@ -17,7 +17,7 @@ * - Allows using ChatGPT subscription for Codex */ -import { Codex, type Thread, type ThreadEvent, type ModelReasoningEffort } from "@openai/codex-sdk"; +import { Codex, type Thread, type ModelReasoningEffort } from "@openai/codex-sdk"; import type { SDKAdapter, SDKConfig, PromptConfig, PromptEvent } from "../types.js"; import { createTranslatorState, @@ -35,11 +35,50 @@ import * as os from "node:os"; import { WORKSPACE_DIR } from "../../constants.js"; import { buildSystemPromptText } from "../../utils/system-prompt.js"; +// Codex session ID mapping (Netclode session ID -> Codex thread ID) +const codexThreadMap = new Map(); +const NETCLODE_PLACEHOLDER_PREFIX = "NETCLODE_PLACEHOLDER_"; + +type CodexAuthMode = "api" | "oauth" | "unknown"; + +function hasCodexApiSuffix(model?: string): boolean { + if (!model) return false; + return /:api(?::(low|medium|high|minimal|xhigh))?$/.test(model); +} + +function hasCodexOAuthSuffix(model?: string): boolean { + if (!model) return false; + return /:oauth(?::(low|medium|high|minimal|xhigh))?$/.test(model); +} + +function isUsableApiKey(apiKey?: string): boolean { + if (!apiKey) return false; + return !apiKey.startsWith(NETCLODE_PLACEHOLDER_PREFIX); +} + +export function resolveCodexAuthMode(config: SDKConfig): CodexAuthMode { + if (hasCodexApiSuffix(config.model)) { + return "api"; + } + if (hasCodexOAuthSuffix(config.model)) { + return "oauth"; + } + + // Without an explicit suffix, prefer OAuth if both token types are available. + if (config.codexAccessToken && config.codexIdToken) { + return "oauth"; + } + if (isUsableApiKey(config.openaiApiKey)) { + return "api"; + } + return "unknown"; +} export class CodexAdapter implements SDKAdapter { private config: SDKConfig | null = null; private codex: Codex | null = null; private thread: Thread | null = null; private interruptSignal = false; + private abortController: AbortController | null = null; private translatorState: TranslatorState = createTranslatorState(); // Cleaned model name (without :api/:oauth/:effort suffixes) @@ -56,11 +95,11 @@ export class CodexAdapter implements SDKAdapter { this.cleanedModel = config.model?.replace(/:(api|oauth)(:(low|medium|high|minimal|xhigh))?$/, ""); this.reasoningEffort = config.reasoningEffort; - // Determine auth mode from model suffix or available credentials - const modelHasApiSuffix = config.model?.includes(":api"); - const modelHasOAuthSuffix = config.model?.includes(":oauth"); - const isApiMode = modelHasApiSuffix || Boolean(config.openaiApiKey && !config.codexAccessToken); - const isOAuthMode = modelHasOAuthSuffix || Boolean(config.codexAccessToken && !config.openaiApiKey); + // Determine auth mode from model suffix or available credentials. + const authMode = resolveCodexAuthMode(config); + const isApiMode = authMode === "api"; + const isOAuthMode = authMode === "oauth"; + const apiKey = isUsableApiKey(config.openaiApiKey) ? config.openaiApiKey : undefined; console.log("[codex-adapter] Initializing"); console.log("[codex-adapter] Model:", this.cleanedModel || "default"); @@ -89,28 +128,28 @@ export class CodexAdapter implements SDKAdapter { if (isOAuthMode && config.codexAccessToken && config.codexIdToken) { // OAuth mode: write tokens to ~/.codex/auth.json // The Codex CLI binary reads credentials from this location - await this.writeCodexAuth(config.codexAccessToken, config.codexIdToken, config.codexRefreshToken); + await this.writeCodexAuth(config.codexAccessToken, config.codexIdToken); console.log("[codex-adapter] Using OAuth authentication (ChatGPT subscription)"); this.codex = new Codex({ // For OAuth, don't pass apiKey - let it use auth.json - // Remove any OPENAI_API_KEY to force OAuth - env: buildEnv({ OPENAI_API_KEY: undefined }), + // Remove any API-key env vars to force OAuth. + env: buildEnv({ OPENAI_API_KEY: undefined, CODEX_API_KEY: undefined }), }); - } else if (isApiMode && config.openaiApiKey) { + } else if (isApiMode && apiKey) { // API key mode: use OPENAI_API_KEY console.log("[codex-adapter] Using API key authentication"); this.codex = new Codex({ - apiKey: config.openaiApiKey, - env: buildEnv({ OPENAI_API_KEY: config.openaiApiKey }), + apiKey, + env: buildEnv({ OPENAI_API_KEY: apiKey }), }); } else { - // Fallback: use environment variable - console.log("[codex-adapter] Using environment OPENAI_API_KEY"); + // Fallback: avoid placeholder API keys and rely on existing Codex auth state. + console.log("[codex-adapter] No explicit auth credentials, using existing Codex auth state"); this.codex = new Codex({ - env: buildEnv(), + env: buildEnv({ OPENAI_API_KEY: undefined, CODEX_API_KEY: undefined }), }); } @@ -139,15 +178,19 @@ export class CodexAdapter implements SDKAdapter { * Write OAuth tokens to Codex auth file * The Codex CLI reads from ~/.codex/auth.json */ - private async writeCodexAuth(accessToken: string, idToken: string, refreshToken?: string): Promise { + private async writeCodexAuth(accessToken: string, idToken: string): Promise { const codexHome = process.env.CODEX_HOME || path.join(os.homedir(), ".codex"); await fs.mkdir(codexHome, { recursive: true }); + const accountId = this.extractAccountIdFromIdToken(idToken); const authData = { + auth_mode: "chatgptAuthTokens", + OPENAI_API_KEY: null, tokens: { access_token: accessToken, id_token: idToken, - refresh_token: refreshToken || "", + refresh_token: "", + account_id: accountId, }, last_refresh: new Date().toISOString(), }; @@ -157,6 +200,32 @@ export class CodexAdapter implements SDKAdapter { console.log("[codex-adapter] OAuth tokens written to", authPath); } + private extractAccountIdFromIdToken(idToken: string): string | undefined { + const parts = idToken.split("."); + if (parts.length !== 3) { + return undefined; + } + try { + const payload = JSON.parse(Buffer.from(parts[1], "base64url").toString("utf-8")); + const authClaims = payload?.["https://api.openai.com/auth"]; + if (!authClaims || typeof authClaims !== "object") { + return undefined; + } + return typeof authClaims.chatgpt_account_id === "string" ? authClaims.chatgpt_account_id : undefined; + } catch { + return undefined; + } + } + + async updateCodexAuth(accessToken: string, idToken: string, _expiresAt?: Date): Promise { + await this.writeCodexAuth(accessToken, idToken); + if (this.config) { + this.config.codexAccessToken = accessToken; + this.config.codexIdToken = idToken; + } + console.log("[codex-adapter] Updated OAuth tokens"); + } + async *executePrompt(sessionId: string, text: string, promptConfig?: PromptConfig): AsyncGenerator { if (!this.codex) { throw new Error("Codex client not initialized"); @@ -175,6 +244,7 @@ export class CodexAdapter implements SDKAdapter { // Clear interrupt signal this.clearInterruptSignal(); + this.abortController = new AbortController(); // Get or create Codex thread (persisted mapping survives pod restarts) const existingThreadId = getSdkSessionId(sessionId); @@ -213,11 +283,14 @@ export class CodexAdapter implements SDKAdapter { try { // Run the prompt with streaming - const { events } = await this.thread.runStreamed(text); + const { events } = await this.thread.runStreamed(text, { + signal: this.abortController.signal, + }); for await (const event of events) { if (this.interruptSignal) { - yield { type: "system", message: "interrupted" }; + console.log("[codex-adapter] Interrupted by user"); + yield { type: "error", message: "Prompt interrupted", retryable: true }; return; } @@ -248,22 +321,35 @@ export class CodexAdapter implements SDKAdapter { // Emit final result yield createResultEvent(this.translatorState); } catch (error) { + if (this.interruptSignal || this.isAbortError(error)) { + console.log("[codex-adapter] Prompt interrupted"); + yield { type: "error", message: "Prompt interrupted", retryable: true }; + return; + } console.error("[codex-adapter] Error during prompt execution:", error); yield { type: "error", message: `Prompt execution error: ${error instanceof Error ? error.message : String(error)}`, retryable: false, }; + } finally { + this.abortController = null; } } setInterruptSignal(): void { this.interruptSignal = true; - console.log("[codex-adapter] Interrupt signal set"); + if (this.abortController) { + this.abortController.abort(); + console.log("[codex-adapter] Interrupt signal set and run aborted"); + } else { + console.log("[codex-adapter] Interrupt signal set"); + } } clearInterruptSignal(): void { this.interruptSignal = false; + this.abortController = null; resetTranslatorState(this.translatorState); } @@ -273,8 +359,25 @@ export class CodexAdapter implements SDKAdapter { async shutdown(): Promise { console.log("[codex-adapter] Shutting down..."); + if (this.abortController) { + this.abortController.abort(); + this.abortController = null; + } this.thread = null; this.codex = null; resetTranslatorState(this.translatorState); } + + private isAbortError(error: unknown): boolean { + if (!(error instanceof Error)) { + return false; + } + + if (error.name === "AbortError") { + return true; + } + + const message = error.message.toLowerCase(); + return message.includes("aborted") || message.includes("aborterror"); + } } diff --git a/services/agent/src/sdk/types.ts b/services/agent/src/sdk/types.ts index fb814ef4..155b7a32 100644 --- a/services/agent/src/sdk/types.ts +++ b/services/agent/src/sdk/types.ts @@ -29,7 +29,6 @@ export interface SDKConfig { // Codex SDK OAuth tokens (for ChatGPT auth mode) codexAccessToken?: string; codexIdToken?: string; - codexRefreshToken?: string; // Codex reasoning effort (low, medium, high, minimal, xhigh) reasoningEffort?: string; // Ollama URL for local inference (e.g., "http://ollama.netclode.svc.cluster.local:11434") @@ -102,4 +101,10 @@ export interface SDKAdapter { * Called when the agent is shutting down */ shutdown(): Promise; + + /** + * Update short-lived Codex OAuth tokens for running sessions. + * Only implemented by the Codex adapter. + */ + updateCodexAuth?(accessToken: string, idToken: string, expiresAt?: Date): Promise; } diff --git a/services/control-plane/Dockerfile b/services/control-plane/Dockerfile index 2d149a19..ffed6efe 100644 --- a/services/control-plane/Dockerfile +++ b/services/control-plane/Dockerfile @@ -3,7 +3,11 @@ FROM golang:1.25-alpine AS builder WORKDIR /app COPY go.mod go.sum ./ -COPY . . +RUN --mount=type=cache,target=/go/pkg/mod go mod download + +COPY cmd ./cmd +COPY internal ./internal +COPY gen ./gen RUN --mount=type=cache,target=/go/pkg/mod \ --mount=type=cache,target=/root/.cache/go-build \ diff --git a/services/control-plane/gen/netclode/v1/agent.pb.go b/services/control-plane/gen/netclode/v1/agent.pb.go index edf56cc5..c3896ca6 100644 --- a/services/control-plane/gen/netclode/v1/agent.pb.go +++ b/services/control-plane/gen/netclode/v1/agent.pb.go @@ -9,6 +9,7 @@ package netclodev1 import ( protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" + timestamppb "google.golang.org/protobuf/types/known/timestamppb" reflect "reflect" sync "sync" unsafe "unsafe" @@ -188,6 +189,7 @@ type ControlPlaneMessage struct { // *ControlPlaneMessage_TerminalInput // *ControlPlaneMessage_UpdateGitCredentials // *ControlPlaneMessage_SessionAssigned + // *ControlPlaneMessage_UpdateCodexAuth Message isControlPlaneMessage_Message `protobuf_oneof:"message"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache @@ -311,6 +313,15 @@ func (x *ControlPlaneMessage) GetSessionAssigned() *SessionAssigned { return nil } +func (x *ControlPlaneMessage) GetUpdateCodexAuth() *UpdateCodexAuth { + if x != nil { + if x, ok := x.Message.(*ControlPlaneMessage_UpdateCodexAuth); ok { + return x.UpdateCodexAuth + } + } + return nil +} + type isControlPlaneMessage_Message interface { isControlPlaneMessage_Message() } @@ -360,6 +371,11 @@ type ControlPlaneMessage_SessionAssigned struct { SessionAssigned *SessionAssigned `protobuf:"bytes,9,opt,name=session_assigned,json=sessionAssigned,proto3,oneof"` } +type ControlPlaneMessage_UpdateCodexAuth struct { + // Update Codex OAuth tokens for an active session. + UpdateCodexAuth *UpdateCodexAuth `protobuf:"bytes,10,opt,name=update_codex_auth,json=updateCodexAuth,proto3,oneof"` +} + func (*ControlPlaneMessage_Registered) isControlPlaneMessage_Message() {} func (*ControlPlaneMessage_ExecutePrompt) isControlPlaneMessage_Message() {} @@ -378,6 +394,8 @@ func (*ControlPlaneMessage_UpdateGitCredentials) isControlPlaneMessage_Message() func (*ControlPlaneMessage_SessionAssigned) isControlPlaneMessage_Message() {} +func (*ControlPlaneMessage_UpdateCodexAuth) isControlPlaneMessage_Message() {} + // SessionAssigned is sent to warm pool agents when a session is bound. // This replaces the HTTP polling approach for instant session start. type SessionAssigned struct { @@ -1540,11 +1558,72 @@ func (x *UpdateGitCredentials) GetRepoAccess() RepoAccess { return RepoAccess_REPO_ACCESS_UNSPECIFIED } +// UpdateCodexAuth updates short-lived Codex OAuth tokens for the running agent. +type UpdateCodexAuth struct { + state protoimpl.MessageState `protogen:"open.v1"` + AccessToken string `protobuf:"bytes,1,opt,name=access_token,json=accessToken,proto3" json:"access_token,omitempty"` + IdToken string `protobuf:"bytes,2,opt,name=id_token,json=idToken,proto3" json:"id_token,omitempty"` + ExpiresAt *timestamppb.Timestamp `protobuf:"bytes,3,opt,name=expires_at,json=expiresAt,proto3,oneof" json:"expires_at,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UpdateCodexAuth) Reset() { + *x = UpdateCodexAuth{} + mi := &file_netclode_v1_agent_proto_msgTypes[22] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UpdateCodexAuth) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UpdateCodexAuth) ProtoMessage() {} + +func (x *UpdateCodexAuth) ProtoReflect() protoreflect.Message { + mi := &file_netclode_v1_agent_proto_msgTypes[22] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UpdateCodexAuth.ProtoReflect.Descriptor instead. +func (*UpdateCodexAuth) Descriptor() ([]byte, []int) { + return file_netclode_v1_agent_proto_rawDescGZIP(), []int{22} +} + +func (x *UpdateCodexAuth) GetAccessToken() string { + if x != nil { + return x.AccessToken + } + return "" +} + +func (x *UpdateCodexAuth) GetIdToken() string { + if x != nil { + return x.IdToken + } + return "" +} + +func (x *UpdateCodexAuth) GetExpiresAt() *timestamppb.Timestamp { + if x != nil { + return x.ExpiresAt + } + return nil +} + var File_netclode_v1_agent_proto protoreflect.FileDescriptor const file_netclode_v1_agent_proto_rawDesc = "" + "\n" + - "\x17netclode/v1/agent.proto\x12\vnetclode.v1\x1a\x18netclode/v1/common.proto\x1a\x18netclode/v1/events.proto\"\xdf\x03\n" + + "\x17netclode/v1/agent.proto\x12\vnetclode.v1\x1a\x18netclode/v1/common.proto\x1a\x18netclode/v1/events.proto\x1a\x1fgoogle/protobuf/timestamp.proto\"\xdf\x03\n" + "\fAgentMessage\x128\n" + "\bregister\x18\x01 \x01(\v2\x1a.netclode.v1.AgentRegisterH\x00R\bregister\x12K\n" + "\x0fprompt_response\x18\x02 \x01(\v2 .netclode.v1.AgentStreamResponseH\x00R\x0epromptResponse\x12K\n" + @@ -1552,7 +1631,7 @@ const file_netclode_v1_agent_proto_rawDesc = "" + "\x0etitle_response\x18\x04 \x01(\v2\x1f.netclode.v1.AgentTitleResponseH\x00R\rtitleResponse\x12U\n" + "\x13git_status_response\x18\x05 \x01(\v2#.netclode.v1.AgentGitStatusResponseH\x00R\x11gitStatusResponse\x12O\n" + "\x11git_diff_response\x18\x06 \x01(\v2!.netclode.v1.AgentGitDiffResponseH\x00R\x0fgitDiffResponseB\t\n" + - "\amessage\"\xb5\x05\n" + + "\amessage\"\x81\x06\n" + "\x13ControlPlaneMessage\x12>\n" + "\n" + "registered\x18\x01 \x01(\v2\x1c.netclode.v1.AgentRegisteredH\x00R\n" + @@ -1565,7 +1644,9 @@ const file_netclode_v1_agent_proto_rawDesc = "" + "getGitDiff\x12H\n" + "\x0eterminal_input\x18\a \x01(\v2\x1f.netclode.v1.AgentTerminalInputH\x00R\rterminalInput\x12Y\n" + "\x16update_git_credentials\x18\b \x01(\v2!.netclode.v1.UpdateGitCredentialsH\x00R\x14updateGitCredentials\x12I\n" + - "\x10session_assigned\x18\t \x01(\v2\x1c.netclode.v1.SessionAssignedH\x00R\x0fsessionAssignedB\t\n" + + "\x10session_assigned\x18\t \x01(\v2\x1c.netclode.v1.SessionAssignedH\x00R\x0fsessionAssigned\x12J\n" + + "\x11update_codex_auth\x18\n" + + " \x01(\v2\x1c.netclode.v1.UpdateCodexAuthH\x00R\x0fupdateCodexAuthB\t\n" + "\amessage\"d\n" + "\x0fSessionAssigned\x12\x1d\n" + "\n" + @@ -1651,7 +1732,13 @@ const file_netclode_v1_agent_proto_rawDesc = "" + "\x14UpdateGitCredentials\x12!\n" + "\fgithub_token\x18\x01 \x01(\tR\vgithubToken\x128\n" + "\vrepo_access\x18\x02 \x01(\x0e2\x17.netclode.v1.RepoAccessR\n" + - "repoAccess2Z\n" + + "repoAccess\"\x9e\x01\n" + + "\x0fUpdateCodexAuth\x12!\n" + + "\faccess_token\x18\x01 \x01(\tR\vaccessToken\x12\x19\n" + + "\bid_token\x18\x02 \x01(\tR\aidToken\x12>\n" + + "\n" + + "expires_at\x18\x03 \x01(\v2\x1a.google.protobuf.TimestampH\x00R\texpiresAt\x88\x01\x01B\r\n" + + "\v_expires_at2Z\n" + "\fAgentService\x12J\n" + "\aConnect\x12\x19.netclode.v1.AgentMessage\x1a .netclode.v1.ControlPlaneMessage(\x010\x01B\xbb\x01\n" + "\x0fcom.netclode.v1B\n" + @@ -1669,7 +1756,7 @@ func file_netclode_v1_agent_proto_rawDescGZIP() []byte { return file_netclode_v1_agent_proto_rawDescData } -var file_netclode_v1_agent_proto_msgTypes = make([]protoimpl.MessageInfo, 22) +var file_netclode_v1_agent_proto_msgTypes = make([]protoimpl.MessageInfo, 23) var file_netclode_v1_agent_proto_goTypes = []any{ (*AgentMessage)(nil), // 0: netclode.v1.AgentMessage (*ControlPlaneMessage)(nil), // 1: netclode.v1.ControlPlaneMessage @@ -1693,10 +1780,12 @@ var file_netclode_v1_agent_proto_goTypes = []any{ (*AgentTerminalInput)(nil), // 19: netclode.v1.AgentTerminalInput (*AgentTerminalResize)(nil), // 20: netclode.v1.AgentTerminalResize (*UpdateGitCredentials)(nil), // 21: netclode.v1.UpdateGitCredentials - (*SessionConfig)(nil), // 22: netclode.v1.SessionConfig - (*AgentEvent)(nil), // 23: netclode.v1.AgentEvent - (*GitFileChange)(nil), // 24: netclode.v1.GitFileChange - (RepoAccess)(0), // 25: netclode.v1.RepoAccess + (*UpdateCodexAuth)(nil), // 22: netclode.v1.UpdateCodexAuth + (*SessionConfig)(nil), // 23: netclode.v1.SessionConfig + (*AgentEvent)(nil), // 24: netclode.v1.AgentEvent + (*GitFileChange)(nil), // 25: netclode.v1.GitFileChange + (RepoAccess)(0), // 26: netclode.v1.RepoAccess + (*timestamppb.Timestamp)(nil), // 27: google.protobuf.Timestamp } var file_netclode_v1_agent_proto_depIdxs = []int32{ 3, // 0: netclode.v1.AgentMessage.register:type_name -> netclode.v1.AgentRegister @@ -1714,23 +1803,25 @@ var file_netclode_v1_agent_proto_depIdxs = []int32{ 19, // 12: netclode.v1.ControlPlaneMessage.terminal_input:type_name -> netclode.v1.AgentTerminalInput 21, // 13: netclode.v1.ControlPlaneMessage.update_git_credentials:type_name -> netclode.v1.UpdateGitCredentials 2, // 14: netclode.v1.ControlPlaneMessage.session_assigned:type_name -> netclode.v1.SessionAssigned - 22, // 15: netclode.v1.SessionAssigned.config:type_name -> netclode.v1.SessionConfig - 5, // 16: netclode.v1.AgentStreamResponse.text_delta:type_name -> netclode.v1.AgentTextDelta - 23, // 17: netclode.v1.AgentStreamResponse.event:type_name -> netclode.v1.AgentEvent - 6, // 18: netclode.v1.AgentStreamResponse.system_message:type_name -> netclode.v1.AgentSystemMessage - 7, // 19: netclode.v1.AgentStreamResponse.result:type_name -> netclode.v1.AgentResult - 8, // 20: netclode.v1.AgentStreamResponse.error:type_name -> netclode.v1.AgentError - 24, // 21: netclode.v1.AgentGitStatusResponse.files:type_name -> netclode.v1.GitFileChange - 22, // 22: netclode.v1.AgentRegistered.config:type_name -> netclode.v1.SessionConfig - 20, // 23: netclode.v1.AgentTerminalInput.resize:type_name -> netclode.v1.AgentTerminalResize - 25, // 24: netclode.v1.UpdateGitCredentials.repo_access:type_name -> netclode.v1.RepoAccess - 0, // 25: netclode.v1.AgentService.Connect:input_type -> netclode.v1.AgentMessage - 1, // 26: netclode.v1.AgentService.Connect:output_type -> netclode.v1.ControlPlaneMessage - 26, // [26:27] is the sub-list for method output_type - 25, // [25:26] is the sub-list for method input_type - 25, // [25:25] is the sub-list for extension type_name - 25, // [25:25] is the sub-list for extension extendee - 0, // [0:25] is the sub-list for field type_name + 22, // 15: netclode.v1.ControlPlaneMessage.update_codex_auth:type_name -> netclode.v1.UpdateCodexAuth + 23, // 16: netclode.v1.SessionAssigned.config:type_name -> netclode.v1.SessionConfig + 5, // 17: netclode.v1.AgentStreamResponse.text_delta:type_name -> netclode.v1.AgentTextDelta + 24, // 18: netclode.v1.AgentStreamResponse.event:type_name -> netclode.v1.AgentEvent + 6, // 19: netclode.v1.AgentStreamResponse.system_message:type_name -> netclode.v1.AgentSystemMessage + 7, // 20: netclode.v1.AgentStreamResponse.result:type_name -> netclode.v1.AgentResult + 8, // 21: netclode.v1.AgentStreamResponse.error:type_name -> netclode.v1.AgentError + 25, // 22: netclode.v1.AgentGitStatusResponse.files:type_name -> netclode.v1.GitFileChange + 23, // 23: netclode.v1.AgentRegistered.config:type_name -> netclode.v1.SessionConfig + 20, // 24: netclode.v1.AgentTerminalInput.resize:type_name -> netclode.v1.AgentTerminalResize + 26, // 25: netclode.v1.UpdateGitCredentials.repo_access:type_name -> netclode.v1.RepoAccess + 27, // 26: netclode.v1.UpdateCodexAuth.expires_at:type_name -> google.protobuf.Timestamp + 0, // 27: netclode.v1.AgentService.Connect:input_type -> netclode.v1.AgentMessage + 1, // 28: netclode.v1.AgentService.Connect:output_type -> netclode.v1.ControlPlaneMessage + 28, // [28:29] is the sub-list for method output_type + 27, // [27:28] is the sub-list for method input_type + 27, // [27:27] is the sub-list for extension type_name + 27, // [27:27] is the sub-list for extension extendee + 0, // [0:27] is the sub-list for field type_name } func init() { file_netclode_v1_agent_proto_init() } @@ -1758,6 +1849,7 @@ func file_netclode_v1_agent_proto_init() { (*ControlPlaneMessage_TerminalInput)(nil), (*ControlPlaneMessage_UpdateGitCredentials)(nil), (*ControlPlaneMessage_SessionAssigned)(nil), + (*ControlPlaneMessage_UpdateCodexAuth)(nil), } file_netclode_v1_agent_proto_msgTypes[3].OneofWrappers = []any{} file_netclode_v1_agent_proto_msgTypes[4].OneofWrappers = []any{ @@ -1773,13 +1865,14 @@ func file_netclode_v1_agent_proto_init() { (*AgentTerminalInput_Data)(nil), (*AgentTerminalInput_Resize)(nil), } + file_netclode_v1_agent_proto_msgTypes[22].OneofWrappers = []any{} type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_netclode_v1_agent_proto_rawDesc), len(file_netclode_v1_agent_proto_rawDesc)), NumEnums: 0, - NumMessages: 22, + NumMessages: 23, NumExtensions: 0, NumServices: 1, }, diff --git a/services/control-plane/gen/netclode/v1/agent_grpc.pb.go b/services/control-plane/gen/netclode/v1/agent_grpc.pb.go index 5282a8f3..62865a0d 100644 --- a/services/control-plane/gen/netclode/v1/agent_grpc.pb.go +++ b/services/control-plane/gen/netclode/v1/agent_grpc.pb.go @@ -1,6 +1,6 @@ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: -// - protoc-gen-go-grpc v1.6.0 +// - protoc-gen-go-grpc v1.6.1 // - protoc (unknown) // source: netclode/v1/agent.proto diff --git a/services/control-plane/gen/netclode/v1/client.pb.go b/services/control-plane/gen/netclode/v1/client.pb.go index bd6be54a..21b19550 100644 --- a/services/control-plane/gen/netclode/v1/client.pb.go +++ b/services/control-plane/gen/netclode/v1/client.pb.go @@ -22,6 +22,61 @@ const ( _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) ) +type CodexAuthState int32 + +const ( + CodexAuthState_CODEX_AUTH_STATE_UNSPECIFIED CodexAuthState = 0 + CodexAuthState_CODEX_AUTH_STATE_UNAUTHENTICATED CodexAuthState = 1 + CodexAuthState_CODEX_AUTH_STATE_PENDING CodexAuthState = 2 + CodexAuthState_CODEX_AUTH_STATE_READY CodexAuthState = 3 + CodexAuthState_CODEX_AUTH_STATE_ERROR CodexAuthState = 4 +) + +// Enum value maps for CodexAuthState. +var ( + CodexAuthState_name = map[int32]string{ + 0: "CODEX_AUTH_STATE_UNSPECIFIED", + 1: "CODEX_AUTH_STATE_UNAUTHENTICATED", + 2: "CODEX_AUTH_STATE_PENDING", + 3: "CODEX_AUTH_STATE_READY", + 4: "CODEX_AUTH_STATE_ERROR", + } + CodexAuthState_value = map[string]int32{ + "CODEX_AUTH_STATE_UNSPECIFIED": 0, + "CODEX_AUTH_STATE_UNAUTHENTICATED": 1, + "CODEX_AUTH_STATE_PENDING": 2, + "CODEX_AUTH_STATE_READY": 3, + "CODEX_AUTH_STATE_ERROR": 4, + } +) + +func (x CodexAuthState) Enum() *CodexAuthState { + p := new(CodexAuthState) + *p = x + return p +} + +func (x CodexAuthState) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (CodexAuthState) Descriptor() protoreflect.EnumDescriptor { + return file_netclode_v1_client_proto_enumTypes[0].Descriptor() +} + +func (CodexAuthState) Type() protoreflect.EnumType { + return &file_netclode_v1_client_proto_enumTypes[0] +} + +func (x CodexAuthState) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use CodexAuthState.Descriptor instead. +func (CodexAuthState) EnumDescriptor() ([]byte, []int) { + return file_netclode_v1_client_proto_rawDescGZIP(), []int{0} +} + // ClientMessage is the union of all client-to-server messages. type ClientMessage struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -49,6 +104,10 @@ type ClientMessage struct { // *ClientMessage_RestoreSnapshot // *ClientMessage_UpdateRepoAccess // *ClientMessage_GetResourceLimits + // *ClientMessage_CodexAuthStart + // *ClientMessage_CodexAuthStatus + // *ClientMessage_CodexAuthLogout + // *ClientMessage_UnexposePort Message isClientMessage_Message `protobuf_oneof:"message"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache @@ -289,6 +348,42 @@ func (x *ClientMessage) GetGetResourceLimits() *GetResourceLimitsRequest { return nil } +func (x *ClientMessage) GetCodexAuthStart() *CodexAuthStartRequest { + if x != nil { + if x, ok := x.Message.(*ClientMessage_CodexAuthStart); ok { + return x.CodexAuthStart + } + } + return nil +} + +func (x *ClientMessage) GetCodexAuthStatus() *CodexAuthStatusRequest { + if x != nil { + if x, ok := x.Message.(*ClientMessage_CodexAuthStatus); ok { + return x.CodexAuthStatus + } + } + return nil +} + +func (x *ClientMessage) GetCodexAuthLogout() *CodexAuthLogoutRequest { + if x != nil { + if x, ok := x.Message.(*ClientMessage_CodexAuthLogout); ok { + return x.CodexAuthLogout + } + } + return nil +} + +func (x *ClientMessage) GetUnexposePort() *UnexposePortRequest { + if x != nil { + if x, ok := x.Message.(*ClientMessage_UnexposePort); ok { + return x.UnexposePort + } + } + return nil +} + type isClientMessage_Message interface { isClientMessage_Message() } @@ -384,6 +479,23 @@ type ClientMessage_GetResourceLimits struct { GetResourceLimits *GetResourceLimitsRequest `protobuf:"bytes,22,opt,name=get_resource_limits,json=getResourceLimits,proto3,oneof"` } +type ClientMessage_CodexAuthStart struct { + // Backend-managed Codex OAuth flow + CodexAuthStart *CodexAuthStartRequest `protobuf:"bytes,23,opt,name=codex_auth_start,json=codexAuthStart,proto3,oneof"` +} + +type ClientMessage_CodexAuthStatus struct { + CodexAuthStatus *CodexAuthStatusRequest `protobuf:"bytes,24,opt,name=codex_auth_status,json=codexAuthStatus,proto3,oneof"` +} + +type ClientMessage_CodexAuthLogout struct { + CodexAuthLogout *CodexAuthLogoutRequest `protobuf:"bytes,25,opt,name=codex_auth_logout,json=codexAuthLogout,proto3,oneof"` +} + +type ClientMessage_UnexposePort struct { + UnexposePort *UnexposePortRequest `protobuf:"bytes,26,opt,name=unexpose_port,json=unexposePort,proto3,oneof"` +} + func (*ClientMessage_CreateSession) isClientMessage_Message() {} func (*ClientMessage_ListSessions) isClientMessage_Message() {} @@ -428,6 +540,14 @@ func (*ClientMessage_UpdateRepoAccess) isClientMessage_Message() {} func (*ClientMessage_GetResourceLimits) isClientMessage_Message() {} +func (*ClientMessage_CodexAuthStart) isClientMessage_Message() {} + +func (*ClientMessage_CodexAuthStatus) isClientMessage_Message() {} + +func (*ClientMessage_CodexAuthLogout) isClientMessage_Message() {} + +func (*ClientMessage_UnexposePort) isClientMessage_Message() {} + // ServerMessage is the union of all server-to-client messages. type ServerMessage struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -453,6 +573,10 @@ type ServerMessage struct { // *ServerMessage_SnapshotRestored // *ServerMessage_RepoAccessUpdated // *ServerMessage_ResourceLimits + // *ServerMessage_CodexAuthStarted + // *ServerMessage_CodexAuthStatus + // *ServerMessage_CodexAuthLoggedOut + // *ServerMessage_PortUnexposed Message isServerMessage_Message `protobuf_oneof:"message"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache @@ -675,6 +799,42 @@ func (x *ServerMessage) GetResourceLimits() *ResourceLimitsResponse { return nil } +func (x *ServerMessage) GetCodexAuthStarted() *CodexAuthStartedResponse { + if x != nil { + if x, ok := x.Message.(*ServerMessage_CodexAuthStarted); ok { + return x.CodexAuthStarted + } + } + return nil +} + +func (x *ServerMessage) GetCodexAuthStatus() *CodexAuthStatusResponse { + if x != nil { + if x, ok := x.Message.(*ServerMessage_CodexAuthStatus); ok { + return x.CodexAuthStatus + } + } + return nil +} + +func (x *ServerMessage) GetCodexAuthLoggedOut() *CodexAuthLoggedOutResponse { + if x != nil { + if x, ok := x.Message.(*ServerMessage_CodexAuthLoggedOut); ok { + return x.CodexAuthLoggedOut + } + } + return nil +} + +func (x *ServerMessage) GetPortUnexposed() *PortUnexposedResponse { + if x != nil { + if x, ok := x.Message.(*ServerMessage_PortUnexposed); ok { + return x.PortUnexposed + } + } + return nil +} + type isServerMessage_Message interface { isServerMessage_Message() } @@ -764,6 +924,23 @@ type ServerMessage_ResourceLimits struct { ResourceLimits *ResourceLimitsResponse `protobuf:"bytes,24,opt,name=resource_limits,json=resourceLimits,proto3,oneof"` } +type ServerMessage_CodexAuthStarted struct { + // Backend-managed Codex OAuth flow + CodexAuthStarted *CodexAuthStartedResponse `protobuf:"bytes,25,opt,name=codex_auth_started,json=codexAuthStarted,proto3,oneof"` +} + +type ServerMessage_CodexAuthStatus struct { + CodexAuthStatus *CodexAuthStatusResponse `protobuf:"bytes,26,opt,name=codex_auth_status,json=codexAuthStatus,proto3,oneof"` +} + +type ServerMessage_CodexAuthLoggedOut struct { + CodexAuthLoggedOut *CodexAuthLoggedOutResponse `protobuf:"bytes,27,opt,name=codex_auth_logged_out,json=codexAuthLoggedOut,proto3,oneof"` +} + +type ServerMessage_PortUnexposed struct { + PortUnexposed *PortUnexposedResponse `protobuf:"bytes,28,opt,name=port_unexposed,json=portUnexposed,proto3,oneof"` +} + func (*ServerMessage_SessionCreated) isServerMessage_Message() {} func (*ServerMessage_SessionUpdated) isServerMessage_Message() {} @@ -804,6 +981,14 @@ func (*ServerMessage_RepoAccessUpdated) isServerMessage_Message() {} func (*ServerMessage_ResourceLimits) isServerMessage_Message() {} +func (*ServerMessage_CodexAuthStarted) isServerMessage_Message() {} + +func (*ServerMessage_CodexAuthStatus) isServerMessage_Message() {} + +func (*ServerMessage_CodexAuthLoggedOut) isServerMessage_Message() {} + +func (*ServerMessage_PortUnexposed) isServerMessage_Message() {} + // NetworkConfig controls network access for a session's sandbox. type NetworkConfig struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -852,25 +1037,95 @@ func (x *NetworkConfig) GetTailnetAccess() bool { return false } +// CodexOAuthTokens contains ChatGPT OAuth tokens for Codex sessions. +type CodexOAuthTokens struct { + state protoimpl.MessageState `protogen:"open.v1"` + AccessToken string `protobuf:"bytes,1,opt,name=access_token,json=accessToken,proto3" json:"access_token,omitempty"` + IdToken string `protobuf:"bytes,2,opt,name=id_token,json=idToken,proto3" json:"id_token,omitempty"` + RefreshToken string `protobuf:"bytes,3,opt,name=refresh_token,json=refreshToken,proto3" json:"refresh_token,omitempty"` + ExpiresAt *timestamppb.Timestamp `protobuf:"bytes,4,opt,name=expires_at,json=expiresAt,proto3,oneof" json:"expires_at,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CodexOAuthTokens) Reset() { + *x = CodexOAuthTokens{} + mi := &file_netclode_v1_client_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CodexOAuthTokens) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CodexOAuthTokens) ProtoMessage() {} + +func (x *CodexOAuthTokens) ProtoReflect() protoreflect.Message { + mi := &file_netclode_v1_client_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CodexOAuthTokens.ProtoReflect.Descriptor instead. +func (*CodexOAuthTokens) Descriptor() ([]byte, []int) { + return file_netclode_v1_client_proto_rawDescGZIP(), []int{3} +} + +func (x *CodexOAuthTokens) GetAccessToken() string { + if x != nil { + return x.AccessToken + } + return "" +} + +func (x *CodexOAuthTokens) GetIdToken() string { + if x != nil { + return x.IdToken + } + return "" +} + +func (x *CodexOAuthTokens) GetRefreshToken() string { + if x != nil { + return x.RefreshToken + } + return "" +} + +func (x *CodexOAuthTokens) GetExpiresAt() *timestamppb.Timestamp { + if x != nil { + return x.ExpiresAt + } + return nil +} + type CreateSessionRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - RequestId *string `protobuf:"bytes,1,opt,name=request_id,json=requestId,proto3,oneof" json:"request_id,omitempty"` // Client-generated ID for request correlation - Name *string `protobuf:"bytes,2,opt,name=name,proto3,oneof" json:"name,omitempty"` // Initial session name - Repos []string `protobuf:"bytes,3,rep,name=repos,proto3" json:"repos,omitempty"` // GitHub repositories to clone (e.g., "owner/repo") - RepoAccess *RepoAccess `protobuf:"varint,4,opt,name=repo_access,json=repoAccess,proto3,enum=netclode.v1.RepoAccess,oneof" json:"repo_access,omitempty"` // Permission level for repository - InitialPrompt *string `protobuf:"bytes,5,opt,name=initial_prompt,json=initialPrompt,proto3,oneof" json:"initial_prompt,omitempty"` // Optional prompt to send immediately after creation - SdkType *SdkType `protobuf:"varint,6,opt,name=sdk_type,json=sdkType,proto3,enum=netclode.v1.SdkType,oneof" json:"sdk_type,omitempty"` // SDK to use (defaults to CLAUDE) - Model *string `protobuf:"bytes,7,opt,name=model,proto3,oneof" json:"model,omitempty"` // Model ID (e.g., "claude-sonnet-4-0", "gpt-4o") - CopilotBackend *CopilotBackend `protobuf:"varint,8,opt,name=copilot_backend,json=copilotBackend,proto3,enum=netclode.v1.CopilotBackend,oneof" json:"copilot_backend,omitempty"` // Backend for Copilot SDK (GitHub or Anthropic) - NetworkConfig *NetworkConfig `protobuf:"bytes,9,opt,name=network_config,json=networkConfig,proto3,oneof" json:"network_config,omitempty"` // Network configuration (defaults to enabled) - Resources *SandboxResources `protobuf:"bytes,10,opt,name=resources,proto3,oneof" json:"resources,omitempty"` // Custom VM resources (bypasses warm pool if set) - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + RequestId *string `protobuf:"bytes,1,opt,name=request_id,json=requestId,proto3,oneof" json:"request_id,omitempty"` // Client-generated ID for request correlation + Name *string `protobuf:"bytes,2,opt,name=name,proto3,oneof" json:"name,omitempty"` // Initial session name + Repos []string `protobuf:"bytes,3,rep,name=repos,proto3" json:"repos,omitempty"` // GitHub repositories to clone (e.g., "owner/repo") + RepoAccess *RepoAccess `protobuf:"varint,4,opt,name=repo_access,json=repoAccess,proto3,enum=netclode.v1.RepoAccess,oneof" json:"repo_access,omitempty"` // Permission level for repository + InitialPrompt *string `protobuf:"bytes,5,opt,name=initial_prompt,json=initialPrompt,proto3,oneof" json:"initial_prompt,omitempty"` // Optional prompt to send immediately after creation + SdkType *SdkType `protobuf:"varint,6,opt,name=sdk_type,json=sdkType,proto3,enum=netclode.v1.SdkType,oneof" json:"sdk_type,omitempty"` // SDK to use (defaults to CLAUDE) + Model *string `protobuf:"bytes,7,opt,name=model,proto3,oneof" json:"model,omitempty"` // Model ID (e.g., "claude-sonnet-4-0", "gpt-4o") + CopilotBackend *CopilotBackend `protobuf:"varint,8,opt,name=copilot_backend,json=copilotBackend,proto3,enum=netclode.v1.CopilotBackend,oneof" json:"copilot_backend,omitempty"` // Backend for Copilot SDK (GitHub or Anthropic) + NetworkConfig *NetworkConfig `protobuf:"bytes,9,opt,name=network_config,json=networkConfig,proto3,oneof" json:"network_config,omitempty"` // Network configuration (defaults to enabled) + Resources *SandboxResources `protobuf:"bytes,10,opt,name=resources,proto3,oneof" json:"resources,omitempty"` // Custom VM resources (bypasses warm pool if set) + CodexOauthTokens *CodexOAuthTokens `protobuf:"bytes,11,opt,name=codex_oauth_tokens,json=codexOauthTokens,proto3,oneof" json:"codex_oauth_tokens,omitempty"` // Session-scoped OAuth tokens for Codex :oauth models + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *CreateSessionRequest) Reset() { *x = CreateSessionRequest{} - mi := &file_netclode_v1_client_proto_msgTypes[3] + mi := &file_netclode_v1_client_proto_msgTypes[4] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -882,7 +1137,7 @@ func (x *CreateSessionRequest) String() string { func (*CreateSessionRequest) ProtoMessage() {} func (x *CreateSessionRequest) ProtoReflect() protoreflect.Message { - mi := &file_netclode_v1_client_proto_msgTypes[3] + mi := &file_netclode_v1_client_proto_msgTypes[4] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -895,7 +1150,7 @@ func (x *CreateSessionRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use CreateSessionRequest.ProtoReflect.Descriptor instead. func (*CreateSessionRequest) Descriptor() ([]byte, []int) { - return file_netclode_v1_client_proto_rawDescGZIP(), []int{3} + return file_netclode_v1_client_proto_rawDescGZIP(), []int{4} } func (x *CreateSessionRequest) GetRequestId() string { @@ -968,6 +1223,13 @@ func (x *CreateSessionRequest) GetResources() *SandboxResources { return nil } +func (x *CreateSessionRequest) GetCodexOauthTokens() *CodexOAuthTokens { + if x != nil { + return x.CodexOauthTokens + } + return nil +} + type ListSessionsRequest struct { state protoimpl.MessageState `protogen:"open.v1"` RequestId *string `protobuf:"bytes,1,opt,name=request_id,json=requestId,proto3,oneof" json:"request_id,omitempty"` @@ -977,7 +1239,7 @@ type ListSessionsRequest struct { func (x *ListSessionsRequest) Reset() { *x = ListSessionsRequest{} - mi := &file_netclode_v1_client_proto_msgTypes[4] + mi := &file_netclode_v1_client_proto_msgTypes[5] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -989,7 +1251,7 @@ func (x *ListSessionsRequest) String() string { func (*ListSessionsRequest) ProtoMessage() {} func (x *ListSessionsRequest) ProtoReflect() protoreflect.Message { - mi := &file_netclode_v1_client_proto_msgTypes[4] + mi := &file_netclode_v1_client_proto_msgTypes[5] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1002,7 +1264,7 @@ func (x *ListSessionsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListSessionsRequest.ProtoReflect.Descriptor instead. func (*ListSessionsRequest) Descriptor() ([]byte, []int) { - return file_netclode_v1_client_proto_rawDescGZIP(), []int{4} + return file_netclode_v1_client_proto_rawDescGZIP(), []int{5} } func (x *ListSessionsRequest) GetRequestId() string { @@ -1024,7 +1286,7 @@ type OpenSessionRequest struct { func (x *OpenSessionRequest) Reset() { *x = OpenSessionRequest{} - mi := &file_netclode_v1_client_proto_msgTypes[5] + mi := &file_netclode_v1_client_proto_msgTypes[6] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1036,7 +1298,7 @@ func (x *OpenSessionRequest) String() string { func (*OpenSessionRequest) ProtoMessage() {} func (x *OpenSessionRequest) ProtoReflect() protoreflect.Message { - mi := &file_netclode_v1_client_proto_msgTypes[5] + mi := &file_netclode_v1_client_proto_msgTypes[6] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1049,7 +1311,7 @@ func (x *OpenSessionRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use OpenSessionRequest.ProtoReflect.Descriptor instead. func (*OpenSessionRequest) Descriptor() ([]byte, []int) { - return file_netclode_v1_client_proto_rawDescGZIP(), []int{5} + return file_netclode_v1_client_proto_rawDescGZIP(), []int{6} } func (x *OpenSessionRequest) GetRequestId() string { @@ -1090,7 +1352,7 @@ type ResumeSessionRequest struct { func (x *ResumeSessionRequest) Reset() { *x = ResumeSessionRequest{} - mi := &file_netclode_v1_client_proto_msgTypes[6] + mi := &file_netclode_v1_client_proto_msgTypes[7] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1102,7 +1364,7 @@ func (x *ResumeSessionRequest) String() string { func (*ResumeSessionRequest) ProtoMessage() {} func (x *ResumeSessionRequest) ProtoReflect() protoreflect.Message { - mi := &file_netclode_v1_client_proto_msgTypes[6] + mi := &file_netclode_v1_client_proto_msgTypes[7] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1115,7 +1377,7 @@ func (x *ResumeSessionRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ResumeSessionRequest.ProtoReflect.Descriptor instead. func (*ResumeSessionRequest) Descriptor() ([]byte, []int) { - return file_netclode_v1_client_proto_rawDescGZIP(), []int{6} + return file_netclode_v1_client_proto_rawDescGZIP(), []int{7} } func (x *ResumeSessionRequest) GetRequestId() string { @@ -1142,7 +1404,7 @@ type PauseSessionRequest struct { func (x *PauseSessionRequest) Reset() { *x = PauseSessionRequest{} - mi := &file_netclode_v1_client_proto_msgTypes[7] + mi := &file_netclode_v1_client_proto_msgTypes[8] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1154,7 +1416,7 @@ func (x *PauseSessionRequest) String() string { func (*PauseSessionRequest) ProtoMessage() {} func (x *PauseSessionRequest) ProtoReflect() protoreflect.Message { - mi := &file_netclode_v1_client_proto_msgTypes[7] + mi := &file_netclode_v1_client_proto_msgTypes[8] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1167,7 +1429,7 @@ func (x *PauseSessionRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use PauseSessionRequest.ProtoReflect.Descriptor instead. func (*PauseSessionRequest) Descriptor() ([]byte, []int) { - return file_netclode_v1_client_proto_rawDescGZIP(), []int{7} + return file_netclode_v1_client_proto_rawDescGZIP(), []int{8} } func (x *PauseSessionRequest) GetRequestId() string { @@ -1194,7 +1456,7 @@ type DeleteSessionRequest struct { func (x *DeleteSessionRequest) Reset() { *x = DeleteSessionRequest{} - mi := &file_netclode_v1_client_proto_msgTypes[8] + mi := &file_netclode_v1_client_proto_msgTypes[9] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1206,7 +1468,7 @@ func (x *DeleteSessionRequest) String() string { func (*DeleteSessionRequest) ProtoMessage() {} func (x *DeleteSessionRequest) ProtoReflect() protoreflect.Message { - mi := &file_netclode_v1_client_proto_msgTypes[8] + mi := &file_netclode_v1_client_proto_msgTypes[9] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1219,7 +1481,7 @@ func (x *DeleteSessionRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use DeleteSessionRequest.ProtoReflect.Descriptor instead. func (*DeleteSessionRequest) Descriptor() ([]byte, []int) { - return file_netclode_v1_client_proto_rawDescGZIP(), []int{8} + return file_netclode_v1_client_proto_rawDescGZIP(), []int{9} } func (x *DeleteSessionRequest) GetRequestId() string { @@ -1245,7 +1507,7 @@ type DeleteAllSessionsRequest struct { func (x *DeleteAllSessionsRequest) Reset() { *x = DeleteAllSessionsRequest{} - mi := &file_netclode_v1_client_proto_msgTypes[9] + mi := &file_netclode_v1_client_proto_msgTypes[10] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1257,7 +1519,7 @@ func (x *DeleteAllSessionsRequest) String() string { func (*DeleteAllSessionsRequest) ProtoMessage() {} func (x *DeleteAllSessionsRequest) ProtoReflect() protoreflect.Message { - mi := &file_netclode_v1_client_proto_msgTypes[9] + mi := &file_netclode_v1_client_proto_msgTypes[10] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1270,7 +1532,7 @@ func (x *DeleteAllSessionsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use DeleteAllSessionsRequest.ProtoReflect.Descriptor instead. func (*DeleteAllSessionsRequest) Descriptor() ([]byte, []int) { - return file_netclode_v1_client_proto_rawDescGZIP(), []int{9} + return file_netclode_v1_client_proto_rawDescGZIP(), []int{10} } func (x *DeleteAllSessionsRequest) GetRequestId() string { @@ -1291,7 +1553,7 @@ type SendPromptRequest struct { func (x *SendPromptRequest) Reset() { *x = SendPromptRequest{} - mi := &file_netclode_v1_client_proto_msgTypes[10] + mi := &file_netclode_v1_client_proto_msgTypes[11] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1303,7 +1565,7 @@ func (x *SendPromptRequest) String() string { func (*SendPromptRequest) ProtoMessage() {} func (x *SendPromptRequest) ProtoReflect() protoreflect.Message { - mi := &file_netclode_v1_client_proto_msgTypes[10] + mi := &file_netclode_v1_client_proto_msgTypes[11] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1316,7 +1578,7 @@ func (x *SendPromptRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use SendPromptRequest.ProtoReflect.Descriptor instead. func (*SendPromptRequest) Descriptor() ([]byte, []int) { - return file_netclode_v1_client_proto_rawDescGZIP(), []int{10} + return file_netclode_v1_client_proto_rawDescGZIP(), []int{11} } func (x *SendPromptRequest) GetRequestId() string { @@ -1350,7 +1612,7 @@ type InterruptPromptRequest struct { func (x *InterruptPromptRequest) Reset() { *x = InterruptPromptRequest{} - mi := &file_netclode_v1_client_proto_msgTypes[11] + mi := &file_netclode_v1_client_proto_msgTypes[12] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1362,7 +1624,7 @@ func (x *InterruptPromptRequest) String() string { func (*InterruptPromptRequest) ProtoMessage() {} func (x *InterruptPromptRequest) ProtoReflect() protoreflect.Message { - mi := &file_netclode_v1_client_proto_msgTypes[11] + mi := &file_netclode_v1_client_proto_msgTypes[12] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1375,7 +1637,7 @@ func (x *InterruptPromptRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use InterruptPromptRequest.ProtoReflect.Descriptor instead. func (*InterruptPromptRequest) Descriptor() ([]byte, []int) { - return file_netclode_v1_client_proto_rawDescGZIP(), []int{11} + return file_netclode_v1_client_proto_rawDescGZIP(), []int{12} } func (x *InterruptPromptRequest) GetRequestId() string { @@ -1403,7 +1665,7 @@ type TerminalInputRequest struct { func (x *TerminalInputRequest) Reset() { *x = TerminalInputRequest{} - mi := &file_netclode_v1_client_proto_msgTypes[12] + mi := &file_netclode_v1_client_proto_msgTypes[13] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1415,7 +1677,7 @@ func (x *TerminalInputRequest) String() string { func (*TerminalInputRequest) ProtoMessage() {} func (x *TerminalInputRequest) ProtoReflect() protoreflect.Message { - mi := &file_netclode_v1_client_proto_msgTypes[12] + mi := &file_netclode_v1_client_proto_msgTypes[13] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1428,7 +1690,7 @@ func (x *TerminalInputRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use TerminalInputRequest.ProtoReflect.Descriptor instead. func (*TerminalInputRequest) Descriptor() ([]byte, []int) { - return file_netclode_v1_client_proto_rawDescGZIP(), []int{12} + return file_netclode_v1_client_proto_rawDescGZIP(), []int{13} } func (x *TerminalInputRequest) GetRequestId() string { @@ -1464,7 +1726,7 @@ type TerminalResizeRequest struct { func (x *TerminalResizeRequest) Reset() { *x = TerminalResizeRequest{} - mi := &file_netclode_v1_client_proto_msgTypes[13] + mi := &file_netclode_v1_client_proto_msgTypes[14] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1476,7 +1738,7 @@ func (x *TerminalResizeRequest) String() string { func (*TerminalResizeRequest) ProtoMessage() {} func (x *TerminalResizeRequest) ProtoReflect() protoreflect.Message { - mi := &file_netclode_v1_client_proto_msgTypes[13] + mi := &file_netclode_v1_client_proto_msgTypes[14] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1489,7 +1751,7 @@ func (x *TerminalResizeRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use TerminalResizeRequest.ProtoReflect.Descriptor instead. func (*TerminalResizeRequest) Descriptor() ([]byte, []int) { - return file_netclode_v1_client_proto_rawDescGZIP(), []int{13} + return file_netclode_v1_client_proto_rawDescGZIP(), []int{14} } func (x *TerminalResizeRequest) GetRequestId() string { @@ -1531,7 +1793,7 @@ type ExposePortRequest struct { func (x *ExposePortRequest) Reset() { *x = ExposePortRequest{} - mi := &file_netclode_v1_client_proto_msgTypes[14] + mi := &file_netclode_v1_client_proto_msgTypes[15] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1543,7 +1805,7 @@ func (x *ExposePortRequest) String() string { func (*ExposePortRequest) ProtoMessage() {} func (x *ExposePortRequest) ProtoReflect() protoreflect.Message { - mi := &file_netclode_v1_client_proto_msgTypes[14] + mi := &file_netclode_v1_client_proto_msgTypes[15] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1556,7 +1818,7 @@ func (x *ExposePortRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ExposePortRequest.ProtoReflect.Descriptor instead. func (*ExposePortRequest) Descriptor() ([]byte, []int) { - return file_netclode_v1_client_proto_rawDescGZIP(), []int{14} + return file_netclode_v1_client_proto_rawDescGZIP(), []int{15} } func (x *ExposePortRequest) GetRequestId() string { @@ -1580,28 +1842,30 @@ func (x *ExposePortRequest) GetPort() int32 { return 0 } -type SyncRequest struct { +type UnexposePortRequest struct { state protoimpl.MessageState `protogen:"open.v1"` RequestId *string `protobuf:"bytes,1,opt,name=request_id,json=requestId,proto3,oneof" json:"request_id,omitempty"` + SessionId string `protobuf:"bytes,2,opt,name=session_id,json=sessionId,proto3" json:"session_id,omitempty"` + Port int32 `protobuf:"varint,3,opt,name=port,proto3" json:"port,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } -func (x *SyncRequest) Reset() { - *x = SyncRequest{} - mi := &file_netclode_v1_client_proto_msgTypes[15] +func (x *UnexposePortRequest) Reset() { + *x = UnexposePortRequest{} + mi := &file_netclode_v1_client_proto_msgTypes[16] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *SyncRequest) String() string { +func (x *UnexposePortRequest) String() string { return protoimpl.X.MessageStringOf(x) } -func (*SyncRequest) ProtoMessage() {} +func (*UnexposePortRequest) ProtoMessage() {} -func (x *SyncRequest) ProtoReflect() protoreflect.Message { - mi := &file_netclode_v1_client_proto_msgTypes[15] +func (x *UnexposePortRequest) ProtoReflect() protoreflect.Message { + mi := &file_netclode_v1_client_proto_msgTypes[16] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1612,29 +1876,87 @@ func (x *SyncRequest) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use SyncRequest.ProtoReflect.Descriptor instead. -func (*SyncRequest) Descriptor() ([]byte, []int) { - return file_netclode_v1_client_proto_rawDescGZIP(), []int{15} +// Deprecated: Use UnexposePortRequest.ProtoReflect.Descriptor instead. +func (*UnexposePortRequest) Descriptor() ([]byte, []int) { + return file_netclode_v1_client_proto_rawDescGZIP(), []int{16} } -func (x *SyncRequest) GetRequestId() string { +func (x *UnexposePortRequest) GetRequestId() string { if x != nil && x.RequestId != nil { return *x.RequestId } return "" } -type ListGitHubReposRequest struct { +func (x *UnexposePortRequest) GetSessionId() string { + if x != nil { + return x.SessionId + } + return "" +} + +func (x *UnexposePortRequest) GetPort() int32 { + if x != nil { + return x.Port + } + return 0 +} + +type SyncRequest struct { state protoimpl.MessageState `protogen:"open.v1"` RequestId *string `protobuf:"bytes,1,opt,name=request_id,json=requestId,proto3,oneof" json:"request_id,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } -func (x *ListGitHubReposRequest) Reset() { - *x = ListGitHubReposRequest{} - mi := &file_netclode_v1_client_proto_msgTypes[16] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) +func (x *SyncRequest) Reset() { + *x = SyncRequest{} + mi := &file_netclode_v1_client_proto_msgTypes[17] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SyncRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SyncRequest) ProtoMessage() {} + +func (x *SyncRequest) ProtoReflect() protoreflect.Message { + mi := &file_netclode_v1_client_proto_msgTypes[17] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SyncRequest.ProtoReflect.Descriptor instead. +func (*SyncRequest) Descriptor() ([]byte, []int) { + return file_netclode_v1_client_proto_rawDescGZIP(), []int{17} +} + +func (x *SyncRequest) GetRequestId() string { + if x != nil && x.RequestId != nil { + return *x.RequestId + } + return "" +} + +type ListGitHubReposRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + RequestId *string `protobuf:"bytes,1,opt,name=request_id,json=requestId,proto3,oneof" json:"request_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListGitHubReposRequest) Reset() { + *x = ListGitHubReposRequest{} + mi := &file_netclode_v1_client_proto_msgTypes[18] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1645,7 +1967,7 @@ func (x *ListGitHubReposRequest) String() string { func (*ListGitHubReposRequest) ProtoMessage() {} func (x *ListGitHubReposRequest) ProtoReflect() protoreflect.Message { - mi := &file_netclode_v1_client_proto_msgTypes[16] + mi := &file_netclode_v1_client_proto_msgTypes[18] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1658,7 +1980,7 @@ func (x *ListGitHubReposRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListGitHubReposRequest.ProtoReflect.Descriptor instead. func (*ListGitHubReposRequest) Descriptor() ([]byte, []int) { - return file_netclode_v1_client_proto_rawDescGZIP(), []int{16} + return file_netclode_v1_client_proto_rawDescGZIP(), []int{18} } func (x *ListGitHubReposRequest) GetRequestId() string { @@ -1678,7 +2000,7 @@ type GitStatusRequest struct { func (x *GitStatusRequest) Reset() { *x = GitStatusRequest{} - mi := &file_netclode_v1_client_proto_msgTypes[17] + mi := &file_netclode_v1_client_proto_msgTypes[19] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1690,7 +2012,7 @@ func (x *GitStatusRequest) String() string { func (*GitStatusRequest) ProtoMessage() {} func (x *GitStatusRequest) ProtoReflect() protoreflect.Message { - mi := &file_netclode_v1_client_proto_msgTypes[17] + mi := &file_netclode_v1_client_proto_msgTypes[19] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1703,7 +2025,7 @@ func (x *GitStatusRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GitStatusRequest.ProtoReflect.Descriptor instead. func (*GitStatusRequest) Descriptor() ([]byte, []int) { - return file_netclode_v1_client_proto_rawDescGZIP(), []int{17} + return file_netclode_v1_client_proto_rawDescGZIP(), []int{19} } func (x *GitStatusRequest) GetRequestId() string { @@ -1731,7 +2053,7 @@ type GitDiffRequest struct { func (x *GitDiffRequest) Reset() { *x = GitDiffRequest{} - mi := &file_netclode_v1_client_proto_msgTypes[18] + mi := &file_netclode_v1_client_proto_msgTypes[20] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1743,7 +2065,7 @@ func (x *GitDiffRequest) String() string { func (*GitDiffRequest) ProtoMessage() {} func (x *GitDiffRequest) ProtoReflect() protoreflect.Message { - mi := &file_netclode_v1_client_proto_msgTypes[18] + mi := &file_netclode_v1_client_proto_msgTypes[20] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1756,7 +2078,7 @@ func (x *GitDiffRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GitDiffRequest.ProtoReflect.Descriptor instead. func (*GitDiffRequest) Descriptor() ([]byte, []int) { - return file_netclode_v1_client_proto_rawDescGZIP(), []int{18} + return file_netclode_v1_client_proto_rawDescGZIP(), []int{20} } func (x *GitDiffRequest) GetRequestId() string { @@ -1781,17 +2103,18 @@ func (x *GitDiffRequest) GetFile() string { } type ListModelsRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - RequestId *string `protobuf:"bytes,1,opt,name=request_id,json=requestId,proto3,oneof" json:"request_id,omitempty"` - SdkType SdkType `protobuf:"varint,2,opt,name=sdk_type,json=sdkType,proto3,enum=netclode.v1.SdkType" json:"sdk_type,omitempty"` // Which SDK to list models for - CopilotBackend *CopilotBackend `protobuf:"varint,3,opt,name=copilot_backend,json=copilotBackend,proto3,enum=netclode.v1.CopilotBackend,oneof" json:"copilot_backend,omitempty"` // For Copilot: which backend's models to list - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + RequestId *string `protobuf:"bytes,1,opt,name=request_id,json=requestId,proto3,oneof" json:"request_id,omitempty"` + SdkType SdkType `protobuf:"varint,2,opt,name=sdk_type,json=sdkType,proto3,enum=netclode.v1.SdkType" json:"sdk_type,omitempty"` // Which SDK to list models for + CopilotBackend *CopilotBackend `protobuf:"varint,3,opt,name=copilot_backend,json=copilotBackend,proto3,enum=netclode.v1.CopilotBackend,oneof" json:"copilot_backend,omitempty"` // For Copilot: which backend's models to list + CodexOauthAvailable *bool `protobuf:"varint,4,opt,name=codex_oauth_available,json=codexOauthAvailable,proto3,oneof" json:"codex_oauth_available,omitempty"` // Hint from client to include Codex :oauth model variants + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *ListModelsRequest) Reset() { *x = ListModelsRequest{} - mi := &file_netclode_v1_client_proto_msgTypes[19] + mi := &file_netclode_v1_client_proto_msgTypes[21] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1803,7 +2126,7 @@ func (x *ListModelsRequest) String() string { func (*ListModelsRequest) ProtoMessage() {} func (x *ListModelsRequest) ProtoReflect() protoreflect.Message { - mi := &file_netclode_v1_client_proto_msgTypes[19] + mi := &file_netclode_v1_client_proto_msgTypes[21] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1816,7 +2139,7 @@ func (x *ListModelsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListModelsRequest.ProtoReflect.Descriptor instead. func (*ListModelsRequest) Descriptor() ([]byte, []int) { - return file_netclode_v1_client_proto_rawDescGZIP(), []int{19} + return file_netclode_v1_client_proto_rawDescGZIP(), []int{21} } func (x *ListModelsRequest) GetRequestId() string { @@ -1840,6 +2163,13 @@ func (x *ListModelsRequest) GetCopilotBackend() CopilotBackend { return CopilotBackend_COPILOT_BACKEND_UNSPECIFIED } +func (x *ListModelsRequest) GetCodexOauthAvailable() bool { + if x != nil && x.CodexOauthAvailable != nil { + return *x.CodexOauthAvailable + } + return false +} + type GetCopilotStatusRequest struct { state protoimpl.MessageState `protogen:"open.v1"` RequestId *string `protobuf:"bytes,1,opt,name=request_id,json=requestId,proto3,oneof" json:"request_id,omitempty"` @@ -1849,7 +2179,7 @@ type GetCopilotStatusRequest struct { func (x *GetCopilotStatusRequest) Reset() { *x = GetCopilotStatusRequest{} - mi := &file_netclode_v1_client_proto_msgTypes[20] + mi := &file_netclode_v1_client_proto_msgTypes[22] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1861,7 +2191,7 @@ func (x *GetCopilotStatusRequest) String() string { func (*GetCopilotStatusRequest) ProtoMessage() {} func (x *GetCopilotStatusRequest) ProtoReflect() protoreflect.Message { - mi := &file_netclode_v1_client_proto_msgTypes[20] + mi := &file_netclode_v1_client_proto_msgTypes[22] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1874,7 +2204,7 @@ func (x *GetCopilotStatusRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetCopilotStatusRequest.ProtoReflect.Descriptor instead. func (*GetCopilotStatusRequest) Descriptor() ([]byte, []int) { - return file_netclode_v1_client_proto_rawDescGZIP(), []int{20} + return file_netclode_v1_client_proto_rawDescGZIP(), []int{22} } func (x *GetCopilotStatusRequest) GetRequestId() string { @@ -1894,7 +2224,7 @@ type ListSnapshotsRequest struct { func (x *ListSnapshotsRequest) Reset() { *x = ListSnapshotsRequest{} - mi := &file_netclode_v1_client_proto_msgTypes[21] + mi := &file_netclode_v1_client_proto_msgTypes[23] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1906,7 +2236,7 @@ func (x *ListSnapshotsRequest) String() string { func (*ListSnapshotsRequest) ProtoMessage() {} func (x *ListSnapshotsRequest) ProtoReflect() protoreflect.Message { - mi := &file_netclode_v1_client_proto_msgTypes[21] + mi := &file_netclode_v1_client_proto_msgTypes[23] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1919,7 +2249,7 @@ func (x *ListSnapshotsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListSnapshotsRequest.ProtoReflect.Descriptor instead. func (*ListSnapshotsRequest) Descriptor() ([]byte, []int) { - return file_netclode_v1_client_proto_rawDescGZIP(), []int{21} + return file_netclode_v1_client_proto_rawDescGZIP(), []int{23} } func (x *ListSnapshotsRequest) GetRequestId() string { @@ -1947,7 +2277,7 @@ type RestoreSnapshotRequest struct { func (x *RestoreSnapshotRequest) Reset() { *x = RestoreSnapshotRequest{} - mi := &file_netclode_v1_client_proto_msgTypes[22] + mi := &file_netclode_v1_client_proto_msgTypes[24] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1959,7 +2289,7 @@ func (x *RestoreSnapshotRequest) String() string { func (*RestoreSnapshotRequest) ProtoMessage() {} func (x *RestoreSnapshotRequest) ProtoReflect() protoreflect.Message { - mi := &file_netclode_v1_client_proto_msgTypes[22] + mi := &file_netclode_v1_client_proto_msgTypes[24] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1972,7 +2302,7 @@ func (x *RestoreSnapshotRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use RestoreSnapshotRequest.ProtoReflect.Descriptor instead. func (*RestoreSnapshotRequest) Descriptor() ([]byte, []int) { - return file_netclode_v1_client_proto_rawDescGZIP(), []int{22} + return file_netclode_v1_client_proto_rawDescGZIP(), []int{24} } func (x *RestoreSnapshotRequest) GetRequestId() string { @@ -2007,7 +2337,7 @@ type UpdateRepoAccessRequest struct { func (x *UpdateRepoAccessRequest) Reset() { *x = UpdateRepoAccessRequest{} - mi := &file_netclode_v1_client_proto_msgTypes[23] + mi := &file_netclode_v1_client_proto_msgTypes[25] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2019,7 +2349,7 @@ func (x *UpdateRepoAccessRequest) String() string { func (*UpdateRepoAccessRequest) ProtoMessage() {} func (x *UpdateRepoAccessRequest) ProtoReflect() protoreflect.Message { - mi := &file_netclode_v1_client_proto_msgTypes[23] + mi := &file_netclode_v1_client_proto_msgTypes[25] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2032,7 +2362,7 @@ func (x *UpdateRepoAccessRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use UpdateRepoAccessRequest.ProtoReflect.Descriptor instead. func (*UpdateRepoAccessRequest) Descriptor() ([]byte, []int) { - return file_netclode_v1_client_proto_rawDescGZIP(), []int{23} + return file_netclode_v1_client_proto_rawDescGZIP(), []int{25} } func (x *UpdateRepoAccessRequest) GetRequestId() string { @@ -2065,7 +2395,7 @@ type GetResourceLimitsRequest struct { func (x *GetResourceLimitsRequest) Reset() { *x = GetResourceLimitsRequest{} - mi := &file_netclode_v1_client_proto_msgTypes[24] + mi := &file_netclode_v1_client_proto_msgTypes[26] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2077,7 +2407,7 @@ func (x *GetResourceLimitsRequest) String() string { func (*GetResourceLimitsRequest) ProtoMessage() {} func (x *GetResourceLimitsRequest) ProtoReflect() protoreflect.Message { - mi := &file_netclode_v1_client_proto_msgTypes[24] + mi := &file_netclode_v1_client_proto_msgTypes[26] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2090,7 +2420,7 @@ func (x *GetResourceLimitsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetResourceLimitsRequest.ProtoReflect.Descriptor instead. func (*GetResourceLimitsRequest) Descriptor() ([]byte, []int) { - return file_netclode_v1_client_proto_rawDescGZIP(), []int{24} + return file_netclode_v1_client_proto_rawDescGZIP(), []int{26} } func (x *GetResourceLimitsRequest) GetRequestId() string { @@ -2100,6 +2430,138 @@ func (x *GetResourceLimitsRequest) GetRequestId() string { return "" } +type CodexAuthStartRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + RequestId *string `protobuf:"bytes,1,opt,name=request_id,json=requestId,proto3,oneof" json:"request_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CodexAuthStartRequest) Reset() { + *x = CodexAuthStartRequest{} + mi := &file_netclode_v1_client_proto_msgTypes[27] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CodexAuthStartRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CodexAuthStartRequest) ProtoMessage() {} + +func (x *CodexAuthStartRequest) ProtoReflect() protoreflect.Message { + mi := &file_netclode_v1_client_proto_msgTypes[27] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CodexAuthStartRequest.ProtoReflect.Descriptor instead. +func (*CodexAuthStartRequest) Descriptor() ([]byte, []int) { + return file_netclode_v1_client_proto_rawDescGZIP(), []int{27} +} + +func (x *CodexAuthStartRequest) GetRequestId() string { + if x != nil && x.RequestId != nil { + return *x.RequestId + } + return "" +} + +type CodexAuthStatusRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + RequestId *string `protobuf:"bytes,1,opt,name=request_id,json=requestId,proto3,oneof" json:"request_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CodexAuthStatusRequest) Reset() { + *x = CodexAuthStatusRequest{} + mi := &file_netclode_v1_client_proto_msgTypes[28] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CodexAuthStatusRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CodexAuthStatusRequest) ProtoMessage() {} + +func (x *CodexAuthStatusRequest) ProtoReflect() protoreflect.Message { + mi := &file_netclode_v1_client_proto_msgTypes[28] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CodexAuthStatusRequest.ProtoReflect.Descriptor instead. +func (*CodexAuthStatusRequest) Descriptor() ([]byte, []int) { + return file_netclode_v1_client_proto_rawDescGZIP(), []int{28} +} + +func (x *CodexAuthStatusRequest) GetRequestId() string { + if x != nil && x.RequestId != nil { + return *x.RequestId + } + return "" +} + +type CodexAuthLogoutRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + RequestId *string `protobuf:"bytes,1,opt,name=request_id,json=requestId,proto3,oneof" json:"request_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CodexAuthLogoutRequest) Reset() { + *x = CodexAuthLogoutRequest{} + mi := &file_netclode_v1_client_proto_msgTypes[29] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CodexAuthLogoutRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CodexAuthLogoutRequest) ProtoMessage() {} + +func (x *CodexAuthLogoutRequest) ProtoReflect() protoreflect.Message { + mi := &file_netclode_v1_client_proto_msgTypes[29] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CodexAuthLogoutRequest.ProtoReflect.Descriptor instead. +func (*CodexAuthLogoutRequest) Descriptor() ([]byte, []int) { + return file_netclode_v1_client_proto_rawDescGZIP(), []int{29} +} + +func (x *CodexAuthLogoutRequest) GetRequestId() string { + if x != nil && x.RequestId != nil { + return *x.RequestId + } + return "" +} + type SessionCreatedResponse struct { state protoimpl.MessageState `protogen:"open.v1"` Session *Session `protobuf:"bytes,1,opt,name=session,proto3" json:"session,omitempty"` @@ -2110,7 +2572,7 @@ type SessionCreatedResponse struct { func (x *SessionCreatedResponse) Reset() { *x = SessionCreatedResponse{} - mi := &file_netclode_v1_client_proto_msgTypes[25] + mi := &file_netclode_v1_client_proto_msgTypes[30] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2122,7 +2584,7 @@ func (x *SessionCreatedResponse) String() string { func (*SessionCreatedResponse) ProtoMessage() {} func (x *SessionCreatedResponse) ProtoReflect() protoreflect.Message { - mi := &file_netclode_v1_client_proto_msgTypes[25] + mi := &file_netclode_v1_client_proto_msgTypes[30] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2135,7 +2597,7 @@ func (x *SessionCreatedResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use SessionCreatedResponse.ProtoReflect.Descriptor instead. func (*SessionCreatedResponse) Descriptor() ([]byte, []int) { - return file_netclode_v1_client_proto_rawDescGZIP(), []int{25} + return file_netclode_v1_client_proto_rawDescGZIP(), []int{30} } func (x *SessionCreatedResponse) GetSession() *Session { @@ -2161,7 +2623,7 @@ type SessionUpdatedResponse struct { func (x *SessionUpdatedResponse) Reset() { *x = SessionUpdatedResponse{} - mi := &file_netclode_v1_client_proto_msgTypes[26] + mi := &file_netclode_v1_client_proto_msgTypes[31] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2173,7 +2635,7 @@ func (x *SessionUpdatedResponse) String() string { func (*SessionUpdatedResponse) ProtoMessage() {} func (x *SessionUpdatedResponse) ProtoReflect() protoreflect.Message { - mi := &file_netclode_v1_client_proto_msgTypes[26] + mi := &file_netclode_v1_client_proto_msgTypes[31] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2186,7 +2648,7 @@ func (x *SessionUpdatedResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use SessionUpdatedResponse.ProtoReflect.Descriptor instead. func (*SessionUpdatedResponse) Descriptor() ([]byte, []int) { - return file_netclode_v1_client_proto_rawDescGZIP(), []int{26} + return file_netclode_v1_client_proto_rawDescGZIP(), []int{31} } func (x *SessionUpdatedResponse) GetSession() *Session { @@ -2206,7 +2668,7 @@ type SessionDeletedResponse struct { func (x *SessionDeletedResponse) Reset() { *x = SessionDeletedResponse{} - mi := &file_netclode_v1_client_proto_msgTypes[27] + mi := &file_netclode_v1_client_proto_msgTypes[32] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2218,7 +2680,7 @@ func (x *SessionDeletedResponse) String() string { func (*SessionDeletedResponse) ProtoMessage() {} func (x *SessionDeletedResponse) ProtoReflect() protoreflect.Message { - mi := &file_netclode_v1_client_proto_msgTypes[27] + mi := &file_netclode_v1_client_proto_msgTypes[32] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2231,7 +2693,7 @@ func (x *SessionDeletedResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use SessionDeletedResponse.ProtoReflect.Descriptor instead. func (*SessionDeletedResponse) Descriptor() ([]byte, []int) { - return file_netclode_v1_client_proto_rawDescGZIP(), []int{27} + return file_netclode_v1_client_proto_rawDescGZIP(), []int{32} } func (x *SessionDeletedResponse) GetSessionId() string { @@ -2258,7 +2720,7 @@ type SessionsDeletedAllResponse struct { func (x *SessionsDeletedAllResponse) Reset() { *x = SessionsDeletedAllResponse{} - mi := &file_netclode_v1_client_proto_msgTypes[28] + mi := &file_netclode_v1_client_proto_msgTypes[33] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2270,7 +2732,7 @@ func (x *SessionsDeletedAllResponse) String() string { func (*SessionsDeletedAllResponse) ProtoMessage() {} func (x *SessionsDeletedAllResponse) ProtoReflect() protoreflect.Message { - mi := &file_netclode_v1_client_proto_msgTypes[28] + mi := &file_netclode_v1_client_proto_msgTypes[33] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2283,7 +2745,7 @@ func (x *SessionsDeletedAllResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use SessionsDeletedAllResponse.ProtoReflect.Descriptor instead. func (*SessionsDeletedAllResponse) Descriptor() ([]byte, []int) { - return file_netclode_v1_client_proto_rawDescGZIP(), []int{28} + return file_netclode_v1_client_proto_rawDescGZIP(), []int{33} } func (x *SessionsDeletedAllResponse) GetDeletedIds() []string { @@ -2310,7 +2772,7 @@ type SessionListResponse struct { func (x *SessionListResponse) Reset() { *x = SessionListResponse{} - mi := &file_netclode_v1_client_proto_msgTypes[29] + mi := &file_netclode_v1_client_proto_msgTypes[34] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2322,7 +2784,7 @@ func (x *SessionListResponse) String() string { func (*SessionListResponse) ProtoMessage() {} func (x *SessionListResponse) ProtoReflect() protoreflect.Message { - mi := &file_netclode_v1_client_proto_msgTypes[29] + mi := &file_netclode_v1_client_proto_msgTypes[34] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2335,7 +2797,7 @@ func (x *SessionListResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use SessionListResponse.ProtoReflect.Descriptor instead. func (*SessionListResponse) Descriptor() ([]byte, []int) { - return file_netclode_v1_client_proto_rawDescGZIP(), []int{29} + return file_netclode_v1_client_proto_rawDescGZIP(), []int{34} } func (x *SessionListResponse) GetSessions() []*Session { @@ -2366,7 +2828,7 @@ type SessionStateResponse struct { func (x *SessionStateResponse) Reset() { *x = SessionStateResponse{} - mi := &file_netclode_v1_client_proto_msgTypes[30] + mi := &file_netclode_v1_client_proto_msgTypes[35] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2378,7 +2840,7 @@ func (x *SessionStateResponse) String() string { func (*SessionStateResponse) ProtoMessage() {} func (x *SessionStateResponse) ProtoReflect() protoreflect.Message { - mi := &file_netclode_v1_client_proto_msgTypes[30] + mi := &file_netclode_v1_client_proto_msgTypes[35] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2391,7 +2853,7 @@ func (x *SessionStateResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use SessionStateResponse.ProtoReflect.Descriptor instead. func (*SessionStateResponse) Descriptor() ([]byte, []int) { - return file_netclode_v1_client_proto_rawDescGZIP(), []int{30} + return file_netclode_v1_client_proto_rawDescGZIP(), []int{35} } func (x *SessionStateResponse) GetSession() *Session { @@ -2447,7 +2909,7 @@ type SyncResponse struct { func (x *SyncResponse) Reset() { *x = SyncResponse{} - mi := &file_netclode_v1_client_proto_msgTypes[31] + mi := &file_netclode_v1_client_proto_msgTypes[36] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2459,7 +2921,7 @@ func (x *SyncResponse) String() string { func (*SyncResponse) ProtoMessage() {} func (x *SyncResponse) ProtoReflect() protoreflect.Message { - mi := &file_netclode_v1_client_proto_msgTypes[31] + mi := &file_netclode_v1_client_proto_msgTypes[36] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2472,7 +2934,7 @@ func (x *SyncResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use SyncResponse.ProtoReflect.Descriptor instead. func (*SyncResponse) Descriptor() ([]byte, []int) { - return file_netclode_v1_client_proto_rawDescGZIP(), []int{31} + return file_netclode_v1_client_proto_rawDescGZIP(), []int{36} } func (x *SyncResponse) GetSessions() []*SessionSummary { @@ -2508,7 +2970,7 @@ type StreamEntryResponse struct { func (x *StreamEntryResponse) Reset() { *x = StreamEntryResponse{} - mi := &file_netclode_v1_client_proto_msgTypes[32] + mi := &file_netclode_v1_client_proto_msgTypes[37] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2520,7 +2982,61 @@ func (x *StreamEntryResponse) String() string { func (*StreamEntryResponse) ProtoMessage() {} func (x *StreamEntryResponse) ProtoReflect() protoreflect.Message { - mi := &file_netclode_v1_client_proto_msgTypes[32] + mi := &file_netclode_v1_client_proto_msgTypes[37] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use StreamEntryResponse.ProtoReflect.Descriptor instead. +func (*StreamEntryResponse) Descriptor() ([]byte, []int) { + return file_netclode_v1_client_proto_rawDescGZIP(), []int{37} +} + +func (x *StreamEntryResponse) GetSessionId() string { + if x != nil { + return x.SessionId + } + return "" +} + +func (x *StreamEntryResponse) GetEntry() *StreamEntry { + if x != nil { + return x.Entry + } + return nil +} + +type PortExposedResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + SessionId string `protobuf:"bytes,1,opt,name=session_id,json=sessionId,proto3" json:"session_id,omitempty"` + Port int32 `protobuf:"varint,2,opt,name=port,proto3" json:"port,omitempty"` + PreviewUrl string `protobuf:"bytes,3,opt,name=preview_url,json=previewUrl,proto3" json:"preview_url,omitempty"` + RequestId *string `protobuf:"bytes,4,opt,name=request_id,json=requestId,proto3,oneof" json:"request_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *PortExposedResponse) Reset() { + *x = PortExposedResponse{} + mi := &file_netclode_v1_client_proto_msgTypes[38] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *PortExposedResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PortExposedResponse) ProtoMessage() {} + +func (x *PortExposedResponse) ProtoReflect() protoreflect.Message { + mi := &file_netclode_v1_client_proto_msgTypes[38] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2528,53 +3044,66 @@ func (x *StreamEntryResponse) ProtoReflect() protoreflect.Message { } return ms } - return mi.MessageOf(x) + return mi.MessageOf(x) +} + +// Deprecated: Use PortExposedResponse.ProtoReflect.Descriptor instead. +func (*PortExposedResponse) Descriptor() ([]byte, []int) { + return file_netclode_v1_client_proto_rawDescGZIP(), []int{38} +} + +func (x *PortExposedResponse) GetSessionId() string { + if x != nil { + return x.SessionId + } + return "" } -// Deprecated: Use StreamEntryResponse.ProtoReflect.Descriptor instead. -func (*StreamEntryResponse) Descriptor() ([]byte, []int) { - return file_netclode_v1_client_proto_rawDescGZIP(), []int{32} +func (x *PortExposedResponse) GetPort() int32 { + if x != nil { + return x.Port + } + return 0 } -func (x *StreamEntryResponse) GetSessionId() string { +func (x *PortExposedResponse) GetPreviewUrl() string { if x != nil { - return x.SessionId + return x.PreviewUrl } return "" } -func (x *StreamEntryResponse) GetEntry() *StreamEntry { - if x != nil { - return x.Entry +func (x *PortExposedResponse) GetRequestId() string { + if x != nil && x.RequestId != nil { + return *x.RequestId } - return nil + return "" } -type PortExposedResponse struct { +type PortUnexposedResponse struct { state protoimpl.MessageState `protogen:"open.v1"` SessionId string `protobuf:"bytes,1,opt,name=session_id,json=sessionId,proto3" json:"session_id,omitempty"` Port int32 `protobuf:"varint,2,opt,name=port,proto3" json:"port,omitempty"` - PreviewUrl string `protobuf:"bytes,3,opt,name=preview_url,json=previewUrl,proto3" json:"preview_url,omitempty"` - RequestId *string `protobuf:"bytes,4,opt,name=request_id,json=requestId,proto3,oneof" json:"request_id,omitempty"` + RequestId *string `protobuf:"bytes,3,opt,name=request_id,json=requestId,proto3,oneof" json:"request_id,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } -func (x *PortExposedResponse) Reset() { - *x = PortExposedResponse{} - mi := &file_netclode_v1_client_proto_msgTypes[33] +func (x *PortUnexposedResponse) Reset() { + *x = PortUnexposedResponse{} + mi := &file_netclode_v1_client_proto_msgTypes[39] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *PortExposedResponse) String() string { +func (x *PortUnexposedResponse) String() string { return protoimpl.X.MessageStringOf(x) } -func (*PortExposedResponse) ProtoMessage() {} +func (*PortUnexposedResponse) ProtoMessage() {} -func (x *PortExposedResponse) ProtoReflect() protoreflect.Message { - mi := &file_netclode_v1_client_proto_msgTypes[33] +func (x *PortUnexposedResponse) ProtoReflect() protoreflect.Message { + mi := &file_netclode_v1_client_proto_msgTypes[39] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2585,33 +3114,26 @@ func (x *PortExposedResponse) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use PortExposedResponse.ProtoReflect.Descriptor instead. -func (*PortExposedResponse) Descriptor() ([]byte, []int) { - return file_netclode_v1_client_proto_rawDescGZIP(), []int{33} +// Deprecated: Use PortUnexposedResponse.ProtoReflect.Descriptor instead. +func (*PortUnexposedResponse) Descriptor() ([]byte, []int) { + return file_netclode_v1_client_proto_rawDescGZIP(), []int{39} } -func (x *PortExposedResponse) GetSessionId() string { +func (x *PortUnexposedResponse) GetSessionId() string { if x != nil { return x.SessionId } return "" } -func (x *PortExposedResponse) GetPort() int32 { +func (x *PortUnexposedResponse) GetPort() int32 { if x != nil { return x.Port } return 0 } -func (x *PortExposedResponse) GetPreviewUrl() string { - if x != nil { - return x.PreviewUrl - } - return "" -} - -func (x *PortExposedResponse) GetRequestId() string { +func (x *PortUnexposedResponse) GetRequestId() string { if x != nil && x.RequestId != nil { return *x.RequestId } @@ -2628,7 +3150,7 @@ type GitHubReposResponse struct { func (x *GitHubReposResponse) Reset() { *x = GitHubReposResponse{} - mi := &file_netclode_v1_client_proto_msgTypes[34] + mi := &file_netclode_v1_client_proto_msgTypes[40] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2640,7 +3162,7 @@ func (x *GitHubReposResponse) String() string { func (*GitHubReposResponse) ProtoMessage() {} func (x *GitHubReposResponse) ProtoReflect() protoreflect.Message { - mi := &file_netclode_v1_client_proto_msgTypes[34] + mi := &file_netclode_v1_client_proto_msgTypes[40] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2653,7 +3175,7 @@ func (x *GitHubReposResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GitHubReposResponse.ProtoReflect.Descriptor instead. func (*GitHubReposResponse) Descriptor() ([]byte, []int) { - return file_netclode_v1_client_proto_rawDescGZIP(), []int{34} + return file_netclode_v1_client_proto_rawDescGZIP(), []int{40} } func (x *GitHubReposResponse) GetRepos() []*GitHubRepo { @@ -2681,7 +3203,7 @@ type GitStatusResponse struct { func (x *GitStatusResponse) Reset() { *x = GitStatusResponse{} - mi := &file_netclode_v1_client_proto_msgTypes[35] + mi := &file_netclode_v1_client_proto_msgTypes[41] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2693,7 +3215,7 @@ func (x *GitStatusResponse) String() string { func (*GitStatusResponse) ProtoMessage() {} func (x *GitStatusResponse) ProtoReflect() protoreflect.Message { - mi := &file_netclode_v1_client_proto_msgTypes[35] + mi := &file_netclode_v1_client_proto_msgTypes[41] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2706,7 +3228,7 @@ func (x *GitStatusResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GitStatusResponse.ProtoReflect.Descriptor instead. func (*GitStatusResponse) Descriptor() ([]byte, []int) { - return file_netclode_v1_client_proto_rawDescGZIP(), []int{35} + return file_netclode_v1_client_proto_rawDescGZIP(), []int{41} } func (x *GitStatusResponse) GetSessionId() string { @@ -2741,7 +3263,7 @@ type GitDiffResponse struct { func (x *GitDiffResponse) Reset() { *x = GitDiffResponse{} - mi := &file_netclode_v1_client_proto_msgTypes[36] + mi := &file_netclode_v1_client_proto_msgTypes[42] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2753,7 +3275,7 @@ func (x *GitDiffResponse) String() string { func (*GitDiffResponse) ProtoMessage() {} func (x *GitDiffResponse) ProtoReflect() protoreflect.Message { - mi := &file_netclode_v1_client_proto_msgTypes[36] + mi := &file_netclode_v1_client_proto_msgTypes[42] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2766,7 +3288,7 @@ func (x *GitDiffResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GitDiffResponse.ProtoReflect.Descriptor instead. func (*GitDiffResponse) Descriptor() ([]byte, []int) { - return file_netclode_v1_client_proto_rawDescGZIP(), []int{36} + return file_netclode_v1_client_proto_rawDescGZIP(), []int{42} } func (x *GitDiffResponse) GetSessionId() string { @@ -2802,7 +3324,7 @@ type ErrorResponse struct { func (x *ErrorResponse) Reset() { *x = ErrorResponse{} - mi := &file_netclode_v1_client_proto_msgTypes[37] + mi := &file_netclode_v1_client_proto_msgTypes[43] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2814,7 +3336,7 @@ func (x *ErrorResponse) String() string { func (*ErrorResponse) ProtoMessage() {} func (x *ErrorResponse) ProtoReflect() protoreflect.Message { - mi := &file_netclode_v1_client_proto_msgTypes[37] + mi := &file_netclode_v1_client_proto_msgTypes[43] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2827,7 +3349,7 @@ func (x *ErrorResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ErrorResponse.ProtoReflect.Descriptor instead. func (*ErrorResponse) Descriptor() ([]byte, []int) { - return file_netclode_v1_client_proto_rawDescGZIP(), []int{37} + return file_netclode_v1_client_proto_rawDescGZIP(), []int{43} } func (x *ErrorResponse) GetError() *Error { @@ -2855,7 +3377,7 @@ type ModelsResponse struct { func (x *ModelsResponse) Reset() { *x = ModelsResponse{} - mi := &file_netclode_v1_client_proto_msgTypes[38] + mi := &file_netclode_v1_client_proto_msgTypes[44] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2867,7 +3389,7 @@ func (x *ModelsResponse) String() string { func (*ModelsResponse) ProtoMessage() {} func (x *ModelsResponse) ProtoReflect() protoreflect.Message { - mi := &file_netclode_v1_client_proto_msgTypes[38] + mi := &file_netclode_v1_client_proto_msgTypes[44] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2880,7 +3402,7 @@ func (x *ModelsResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ModelsResponse.ProtoReflect.Descriptor instead. func (*ModelsResponse) Descriptor() ([]byte, []int) { - return file_netclode_v1_client_proto_rawDescGZIP(), []int{38} + return file_netclode_v1_client_proto_rawDescGZIP(), []int{44} } func (x *ModelsResponse) GetModels() []*ModelInfo { @@ -2915,7 +3437,7 @@ type CopilotStatusResponse struct { func (x *CopilotStatusResponse) Reset() { *x = CopilotStatusResponse{} - mi := &file_netclode_v1_client_proto_msgTypes[39] + mi := &file_netclode_v1_client_proto_msgTypes[45] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2927,7 +3449,7 @@ func (x *CopilotStatusResponse) String() string { func (*CopilotStatusResponse) ProtoMessage() {} func (x *CopilotStatusResponse) ProtoReflect() protoreflect.Message { - mi := &file_netclode_v1_client_proto_msgTypes[39] + mi := &file_netclode_v1_client_proto_msgTypes[45] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2940,7 +3462,7 @@ func (x *CopilotStatusResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use CopilotStatusResponse.ProtoReflect.Descriptor instead. func (*CopilotStatusResponse) Descriptor() ([]byte, []int) { - return file_netclode_v1_client_proto_rawDescGZIP(), []int{39} + return file_netclode_v1_client_proto_rawDescGZIP(), []int{45} } func (x *CopilotStatusResponse) GetAuth() *CopilotAuthStatus { @@ -2964,6 +3486,210 @@ func (x *CopilotStatusResponse) GetRequestId() string { return "" } +type CodexAuthStartedResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + VerificationUri string `protobuf:"bytes,1,opt,name=verification_uri,json=verificationUri,proto3" json:"verification_uri,omitempty"` + VerificationUriComplete *string `protobuf:"bytes,2,opt,name=verification_uri_complete,json=verificationUriComplete,proto3,oneof" json:"verification_uri_complete,omitempty"` + UserCode string `protobuf:"bytes,3,opt,name=user_code,json=userCode,proto3" json:"user_code,omitempty"` + IntervalSeconds int32 `protobuf:"varint,4,opt,name=interval_seconds,json=intervalSeconds,proto3" json:"interval_seconds,omitempty"` + ExpiresAt *timestamppb.Timestamp `protobuf:"bytes,5,opt,name=expires_at,json=expiresAt,proto3" json:"expires_at,omitempty"` + RequestId *string `protobuf:"bytes,6,opt,name=request_id,json=requestId,proto3,oneof" json:"request_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CodexAuthStartedResponse) Reset() { + *x = CodexAuthStartedResponse{} + mi := &file_netclode_v1_client_proto_msgTypes[46] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CodexAuthStartedResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CodexAuthStartedResponse) ProtoMessage() {} + +func (x *CodexAuthStartedResponse) ProtoReflect() protoreflect.Message { + mi := &file_netclode_v1_client_proto_msgTypes[46] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CodexAuthStartedResponse.ProtoReflect.Descriptor instead. +func (*CodexAuthStartedResponse) Descriptor() ([]byte, []int) { + return file_netclode_v1_client_proto_rawDescGZIP(), []int{46} +} + +func (x *CodexAuthStartedResponse) GetVerificationUri() string { + if x != nil { + return x.VerificationUri + } + return "" +} + +func (x *CodexAuthStartedResponse) GetVerificationUriComplete() string { + if x != nil && x.VerificationUriComplete != nil { + return *x.VerificationUriComplete + } + return "" +} + +func (x *CodexAuthStartedResponse) GetUserCode() string { + if x != nil { + return x.UserCode + } + return "" +} + +func (x *CodexAuthStartedResponse) GetIntervalSeconds() int32 { + if x != nil { + return x.IntervalSeconds + } + return 0 +} + +func (x *CodexAuthStartedResponse) GetExpiresAt() *timestamppb.Timestamp { + if x != nil { + return x.ExpiresAt + } + return nil +} + +func (x *CodexAuthStartedResponse) GetRequestId() string { + if x != nil && x.RequestId != nil { + return *x.RequestId + } + return "" +} + +type CodexAuthStatusResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + State CodexAuthState `protobuf:"varint,1,opt,name=state,proto3,enum=netclode.v1.CodexAuthState" json:"state,omitempty"` + AccountId *string `protobuf:"bytes,2,opt,name=account_id,json=accountId,proto3,oneof" json:"account_id,omitempty"` + ExpiresAt *timestamppb.Timestamp `protobuf:"bytes,3,opt,name=expires_at,json=expiresAt,proto3,oneof" json:"expires_at,omitempty"` + Error *string `protobuf:"bytes,4,opt,name=error,proto3,oneof" json:"error,omitempty"` + RequestId *string `protobuf:"bytes,5,opt,name=request_id,json=requestId,proto3,oneof" json:"request_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CodexAuthStatusResponse) Reset() { + *x = CodexAuthStatusResponse{} + mi := &file_netclode_v1_client_proto_msgTypes[47] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CodexAuthStatusResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CodexAuthStatusResponse) ProtoMessage() {} + +func (x *CodexAuthStatusResponse) ProtoReflect() protoreflect.Message { + mi := &file_netclode_v1_client_proto_msgTypes[47] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CodexAuthStatusResponse.ProtoReflect.Descriptor instead. +func (*CodexAuthStatusResponse) Descriptor() ([]byte, []int) { + return file_netclode_v1_client_proto_rawDescGZIP(), []int{47} +} + +func (x *CodexAuthStatusResponse) GetState() CodexAuthState { + if x != nil { + return x.State + } + return CodexAuthState_CODEX_AUTH_STATE_UNSPECIFIED +} + +func (x *CodexAuthStatusResponse) GetAccountId() string { + if x != nil && x.AccountId != nil { + return *x.AccountId + } + return "" +} + +func (x *CodexAuthStatusResponse) GetExpiresAt() *timestamppb.Timestamp { + if x != nil { + return x.ExpiresAt + } + return nil +} + +func (x *CodexAuthStatusResponse) GetError() string { + if x != nil && x.Error != nil { + return *x.Error + } + return "" +} + +func (x *CodexAuthStatusResponse) GetRequestId() string { + if x != nil && x.RequestId != nil { + return *x.RequestId + } + return "" +} + +type CodexAuthLoggedOutResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + RequestId *string `protobuf:"bytes,1,opt,name=request_id,json=requestId,proto3,oneof" json:"request_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CodexAuthLoggedOutResponse) Reset() { + *x = CodexAuthLoggedOutResponse{} + mi := &file_netclode_v1_client_proto_msgTypes[48] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CodexAuthLoggedOutResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CodexAuthLoggedOutResponse) ProtoMessage() {} + +func (x *CodexAuthLoggedOutResponse) ProtoReflect() protoreflect.Message { + mi := &file_netclode_v1_client_proto_msgTypes[48] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CodexAuthLoggedOutResponse.ProtoReflect.Descriptor instead. +func (*CodexAuthLoggedOutResponse) Descriptor() ([]byte, []int) { + return file_netclode_v1_client_proto_rawDescGZIP(), []int{48} +} + +func (x *CodexAuthLoggedOutResponse) GetRequestId() string { + if x != nil && x.RequestId != nil { + return *x.RequestId + } + return "" +} + // SnapshotCreatedResponse is pushed to clients when an auto-snapshot is created after a turn. type SnapshotCreatedResponse struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -2975,7 +3701,7 @@ type SnapshotCreatedResponse struct { func (x *SnapshotCreatedResponse) Reset() { *x = SnapshotCreatedResponse{} - mi := &file_netclode_v1_client_proto_msgTypes[40] + mi := &file_netclode_v1_client_proto_msgTypes[49] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2987,7 +3713,7 @@ func (x *SnapshotCreatedResponse) String() string { func (*SnapshotCreatedResponse) ProtoMessage() {} func (x *SnapshotCreatedResponse) ProtoReflect() protoreflect.Message { - mi := &file_netclode_v1_client_proto_msgTypes[40] + mi := &file_netclode_v1_client_proto_msgTypes[49] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3000,7 +3726,7 @@ func (x *SnapshotCreatedResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use SnapshotCreatedResponse.ProtoReflect.Descriptor instead. func (*SnapshotCreatedResponse) Descriptor() ([]byte, []int) { - return file_netclode_v1_client_proto_rawDescGZIP(), []int{40} + return file_netclode_v1_client_proto_rawDescGZIP(), []int{49} } func (x *SnapshotCreatedResponse) GetSessionId() string { @@ -3028,7 +3754,7 @@ type SnapshotListResponse struct { func (x *SnapshotListResponse) Reset() { *x = SnapshotListResponse{} - mi := &file_netclode_v1_client_proto_msgTypes[41] + mi := &file_netclode_v1_client_proto_msgTypes[50] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3040,7 +3766,7 @@ func (x *SnapshotListResponse) String() string { func (*SnapshotListResponse) ProtoMessage() {} func (x *SnapshotListResponse) ProtoReflect() protoreflect.Message { - mi := &file_netclode_v1_client_proto_msgTypes[41] + mi := &file_netclode_v1_client_proto_msgTypes[50] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3053,7 +3779,7 @@ func (x *SnapshotListResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use SnapshotListResponse.ProtoReflect.Descriptor instead. func (*SnapshotListResponse) Descriptor() ([]byte, []int) { - return file_netclode_v1_client_proto_rawDescGZIP(), []int{41} + return file_netclode_v1_client_proto_rawDescGZIP(), []int{50} } func (x *SnapshotListResponse) GetSessionId() string { @@ -3090,7 +3816,7 @@ type SnapshotRestoredResponse struct { func (x *SnapshotRestoredResponse) Reset() { *x = SnapshotRestoredResponse{} - mi := &file_netclode_v1_client_proto_msgTypes[42] + mi := &file_netclode_v1_client_proto_msgTypes[51] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3102,7 +3828,7 @@ func (x *SnapshotRestoredResponse) String() string { func (*SnapshotRestoredResponse) ProtoMessage() {} func (x *SnapshotRestoredResponse) ProtoReflect() protoreflect.Message { - mi := &file_netclode_v1_client_proto_msgTypes[42] + mi := &file_netclode_v1_client_proto_msgTypes[51] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3115,7 +3841,7 @@ func (x *SnapshotRestoredResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use SnapshotRestoredResponse.ProtoReflect.Descriptor instead. func (*SnapshotRestoredResponse) Descriptor() ([]byte, []int) { - return file_netclode_v1_client_proto_rawDescGZIP(), []int{42} + return file_netclode_v1_client_proto_rawDescGZIP(), []int{51} } func (x *SnapshotRestoredResponse) GetSessionId() string { @@ -3158,7 +3884,7 @@ type RepoAccessUpdatedResponse struct { func (x *RepoAccessUpdatedResponse) Reset() { *x = RepoAccessUpdatedResponse{} - mi := &file_netclode_v1_client_proto_msgTypes[43] + mi := &file_netclode_v1_client_proto_msgTypes[52] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3170,7 +3896,7 @@ func (x *RepoAccessUpdatedResponse) String() string { func (*RepoAccessUpdatedResponse) ProtoMessage() {} func (x *RepoAccessUpdatedResponse) ProtoReflect() protoreflect.Message { - mi := &file_netclode_v1_client_proto_msgTypes[43] + mi := &file_netclode_v1_client_proto_msgTypes[52] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3183,7 +3909,7 @@ func (x *RepoAccessUpdatedResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use RepoAccessUpdatedResponse.ProtoReflect.Descriptor instead. func (*RepoAccessUpdatedResponse) Descriptor() ([]byte, []int) { - return file_netclode_v1_client_proto_rawDescGZIP(), []int{43} + return file_netclode_v1_client_proto_rawDescGZIP(), []int{52} } func (x *RepoAccessUpdatedResponse) GetSessionId() string { @@ -3222,7 +3948,7 @@ type ResourceLimitsResponse struct { func (x *ResourceLimitsResponse) Reset() { *x = ResourceLimitsResponse{} - mi := &file_netclode_v1_client_proto_msgTypes[44] + mi := &file_netclode_v1_client_proto_msgTypes[53] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3234,7 +3960,7 @@ func (x *ResourceLimitsResponse) String() string { func (*ResourceLimitsResponse) ProtoMessage() {} func (x *ResourceLimitsResponse) ProtoReflect() protoreflect.Message { - mi := &file_netclode_v1_client_proto_msgTypes[44] + mi := &file_netclode_v1_client_proto_msgTypes[53] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3247,7 +3973,7 @@ func (x *ResourceLimitsResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ResourceLimitsResponse.ProtoReflect.Descriptor instead. func (*ResourceLimitsResponse) Descriptor() ([]byte, []int) { - return file_netclode_v1_client_proto_rawDescGZIP(), []int{44} + return file_netclode_v1_client_proto_rawDescGZIP(), []int{53} } func (x *ResourceLimitsResponse) GetMaxVcpus() int32 { @@ -3289,7 +4015,7 @@ var File_netclode_v1_client_proto protoreflect.FileDescriptor const file_netclode_v1_client_proto_rawDesc = "" + "\n" + - "\x18netclode/v1/client.proto\x12\vnetclode.v1\x1a\x18netclode/v1/common.proto\x1a\x18netclode/v1/events.proto\x1a\x1fgoogle/protobuf/timestamp.proto\"\x85\r\n" + + "\x18netclode/v1/client.proto\x12\vnetclode.v1\x1a\x18netclode/v1/common.proto\x1a\x18netclode/v1/events.proto\x1a\x1fgoogle/protobuf/timestamp.proto\"\xc4\x0f\n" + "\rClientMessage\x12J\n" + "\x0ecreate_session\x18\x01 \x01(\v2!.netclode.v1.CreateSessionRequestH\x00R\rcreateSession\x12G\n" + "\rlist_sessions\x18\x02 \x01(\v2 .netclode.v1.ListSessionsRequestH\x00R\flistSessions\x12D\n" + @@ -3317,8 +4043,12 @@ const file_netclode_v1_client_proto_rawDesc = "" + "\x0elist_snapshots\x18\x13 \x01(\v2!.netclode.v1.ListSnapshotsRequestH\x00R\rlistSnapshots\x12P\n" + "\x10restore_snapshot\x18\x14 \x01(\v2#.netclode.v1.RestoreSnapshotRequestH\x00R\x0frestoreSnapshot\x12T\n" + "\x12update_repo_access\x18\x15 \x01(\v2$.netclode.v1.UpdateRepoAccessRequestH\x00R\x10updateRepoAccess\x12W\n" + - "\x13get_resource_limits\x18\x16 \x01(\v2%.netclode.v1.GetResourceLimitsRequestH\x00R\x11getResourceLimitsB\t\n" + - "\amessage\"\xe0\v\n" + + "\x13get_resource_limits\x18\x16 \x01(\v2%.netclode.v1.GetResourceLimitsRequestH\x00R\x11getResourceLimits\x12N\n" + + "\x10codex_auth_start\x18\x17 \x01(\v2\".netclode.v1.CodexAuthStartRequestH\x00R\x0ecodexAuthStart\x12Q\n" + + "\x11codex_auth_status\x18\x18 \x01(\v2#.netclode.v1.CodexAuthStatusRequestH\x00R\x0fcodexAuthStatus\x12Q\n" + + "\x11codex_auth_logout\x18\x19 \x01(\v2#.netclode.v1.CodexAuthLogoutRequestH\x00R\x0fcodexAuthLogout\x12G\n" + + "\runexpose_port\x18\x1a \x01(\v2 .netclode.v1.UnexposePortRequestH\x00R\funexposePortB\t\n" + + "\amessage\"\xb6\x0e\n" + "\rServerMessage\x12N\n" + "\x0fsession_created\x18\x01 \x01(\v2#.netclode.v1.SessionCreatedResponseH\x00R\x0esessionCreated\x12N\n" + "\x0fsession_updated\x18\x02 \x01(\v2#.netclode.v1.SessionUpdatedResponseH\x00R\x0esessionUpdated\x12N\n" + @@ -3340,10 +4070,21 @@ const file_netclode_v1_client_proto_rawDesc = "" + "\rsnapshot_list\x18\x15 \x01(\v2!.netclode.v1.SnapshotListResponseH\x00R\fsnapshotList\x12T\n" + "\x11snapshot_restored\x18\x16 \x01(\v2%.netclode.v1.SnapshotRestoredResponseH\x00R\x10snapshotRestored\x12X\n" + "\x13repo_access_updated\x18\x17 \x01(\v2&.netclode.v1.RepoAccessUpdatedResponseH\x00R\x11repoAccessUpdated\x12N\n" + - "\x0fresource_limits\x18\x18 \x01(\v2#.netclode.v1.ResourceLimitsResponseH\x00R\x0eresourceLimitsB\t\n" + + "\x0fresource_limits\x18\x18 \x01(\v2#.netclode.v1.ResourceLimitsResponseH\x00R\x0eresourceLimits\x12U\n" + + "\x12codex_auth_started\x18\x19 \x01(\v2%.netclode.v1.CodexAuthStartedResponseH\x00R\x10codexAuthStarted\x12R\n" + + "\x11codex_auth_status\x18\x1a \x01(\v2$.netclode.v1.CodexAuthStatusResponseH\x00R\x0fcodexAuthStatus\x12\\\n" + + "\x15codex_auth_logged_out\x18\x1b \x01(\v2'.netclode.v1.CodexAuthLoggedOutResponseH\x00R\x12codexAuthLoggedOut\x12K\n" + + "\x0eport_unexposed\x18\x1c \x01(\v2\".netclode.v1.PortUnexposedResponseH\x00R\rportUnexposedB\t\n" + "\amessage\"6\n" + "\rNetworkConfig\x12%\n" + - "\x0etailnet_access\x18\x01 \x01(\bR\rtailnetAccess\"\x81\x05\n" + + "\x0etailnet_access\x18\x01 \x01(\bR\rtailnetAccess\"\xc4\x01\n" + + "\x10CodexOAuthTokens\x12!\n" + + "\faccess_token\x18\x01 \x01(\tR\vaccessToken\x12\x19\n" + + "\bid_token\x18\x02 \x01(\tR\aidToken\x12#\n" + + "\rrefresh_token\x18\x03 \x01(\tR\frefreshToken\x12>\n" + + "\n" + + "expires_at\x18\x04 \x01(\v2\x1a.google.protobuf.TimestampH\x00R\texpiresAt\x88\x01\x01B\r\n" + + "\v_expires_at\"\xea\x05\n" + "\x14CreateSessionRequest\x12\"\n" + "\n" + "request_id\x18\x01 \x01(\tH\x00R\trequestId\x88\x01\x01\x12\x17\n" + @@ -3357,7 +4098,8 @@ const file_netclode_v1_client_proto_rawDesc = "" + "\x0fcopilot_backend\x18\b \x01(\x0e2\x1b.netclode.v1.CopilotBackendH\x06R\x0ecopilotBackend\x88\x01\x01\x12F\n" + "\x0enetwork_config\x18\t \x01(\v2\x1a.netclode.v1.NetworkConfigH\aR\rnetworkConfig\x88\x01\x01\x12@\n" + "\tresources\x18\n" + - " \x01(\v2\x1d.netclode.v1.SandboxResourcesH\bR\tresources\x88\x01\x01B\r\n" + + " \x01(\v2\x1d.netclode.v1.SandboxResourcesH\bR\tresources\x88\x01\x01\x12P\n" + + "\x12codex_oauth_tokens\x18\v \x01(\v2\x1d.netclode.v1.CodexOAuthTokensH\tR\x10codexOauthTokens\x88\x01\x01B\r\n" + "\v_request_idB\a\n" + "\x05_nameB\x0e\n" + "\f_repo_accessB\x11\n" + @@ -3367,7 +4109,8 @@ const file_netclode_v1_client_proto_rawDesc = "" + "\x10_copilot_backendB\x11\n" + "\x0f_network_configB\f\n" + "\n" + - "_resources\"H\n" + + "_resourcesB\x15\n" + + "\x13_codex_oauth_tokens\"H\n" + "\x13ListSessionsRequest\x12\"\n" + "\n" + "request_id\x18\x01 \x01(\tH\x00R\trequestId\x88\x01\x01B\r\n" + @@ -3438,6 +4181,13 @@ const file_netclode_v1_client_proto_rawDesc = "" + "\n" + "session_id\x18\x02 \x01(\tR\tsessionId\x12\x12\n" + "\x04port\x18\x03 \x01(\x05R\x04portB\r\n" + + "\v_request_id\"{\n" + + "\x13UnexposePortRequest\x12\"\n" + + "\n" + + "request_id\x18\x01 \x01(\tH\x00R\trequestId\x88\x01\x01\x12\x1d\n" + + "\n" + + "session_id\x18\x02 \x01(\tR\tsessionId\x12\x12\n" + + "\x04port\x18\x03 \x01(\x05R\x04portB\r\n" + "\v_request_id\"@\n" + "\vSyncRequest\x12\"\n" + "\n" + @@ -3460,14 +4210,16 @@ const file_netclode_v1_client_proto_rawDesc = "" + "session_id\x18\x02 \x01(\tR\tsessionId\x12\x17\n" + "\x04file\x18\x03 \x01(\tH\x01R\x04file\x88\x01\x01B\r\n" + "\v_request_idB\a\n" + - "\x05_file\"\xd6\x01\n" + + "\x05_file\"\xa9\x02\n" + "\x11ListModelsRequest\x12\"\n" + "\n" + "request_id\x18\x01 \x01(\tH\x00R\trequestId\x88\x01\x01\x12/\n" + "\bsdk_type\x18\x02 \x01(\x0e2\x14.netclode.v1.SdkTypeR\asdkType\x12I\n" + - "\x0fcopilot_backend\x18\x03 \x01(\x0e2\x1b.netclode.v1.CopilotBackendH\x01R\x0ecopilotBackend\x88\x01\x01B\r\n" + + "\x0fcopilot_backend\x18\x03 \x01(\x0e2\x1b.netclode.v1.CopilotBackendH\x01R\x0ecopilotBackend\x88\x01\x01\x127\n" + + "\x15codex_oauth_available\x18\x04 \x01(\bH\x02R\x13codexOauthAvailable\x88\x01\x01B\r\n" + "\v_request_idB\x12\n" + - "\x10_copilot_backend\"L\n" + + "\x10_copilot_backendB\x18\n" + + "\x16_codex_oauth_available\"L\n" + "\x17GetCopilotStatusRequest\x12\"\n" + "\n" + "request_id\x18\x01 \x01(\tH\x00R\trequestId\x88\x01\x01B\r\n" + @@ -3497,6 +4249,18 @@ const file_netclode_v1_client_proto_rawDesc = "" + "\x18GetResourceLimitsRequest\x12\"\n" + "\n" + "request_id\x18\x01 \x01(\tH\x00R\trequestId\x88\x01\x01B\r\n" + + "\v_request_id\"J\n" + + "\x15CodexAuthStartRequest\x12\"\n" + + "\n" + + "request_id\x18\x01 \x01(\tH\x00R\trequestId\x88\x01\x01B\r\n" + + "\v_request_id\"K\n" + + "\x16CodexAuthStatusRequest\x12\"\n" + + "\n" + + "request_id\x18\x01 \x01(\tH\x00R\trequestId\x88\x01\x01B\r\n" + + "\v_request_id\"K\n" + + "\x16CodexAuthLogoutRequest\x12\"\n" + + "\n" + + "request_id\x18\x01 \x01(\tH\x00R\trequestId\x88\x01\x01B\r\n" + "\v_request_id\"{\n" + "\x16SessionCreatedResponse\x12.\n" + "\asession\x18\x01 \x01(\v2\x14.netclode.v1.SessionR\asession\x12\"\n" + @@ -3553,6 +4317,13 @@ const file_netclode_v1_client_proto_rawDesc = "" + "previewUrl\x12\"\n" + "\n" + "request_id\x18\x04 \x01(\tH\x00R\trequestId\x88\x01\x01B\r\n" + + "\v_request_id\"}\n" + + "\x15PortUnexposedResponse\x12\x1d\n" + + "\n" + + "session_id\x18\x01 \x01(\tR\tsessionId\x12\x12\n" + + "\x04port\x18\x02 \x01(\x05R\x04port\x12\"\n" + + "\n" + + "request_id\x18\x03 \x01(\tH\x00R\trequestId\x88\x01\x01B\r\n" + "\v_request_id\"w\n" + "\x13GitHubReposResponse\x12-\n" + "\x05repos\x18\x01 \x03(\v2\x17.netclode.v1.GitHubRepoR\x05repos\x12\"\n" + @@ -3591,6 +4362,34 @@ const file_netclode_v1_client_proto_rawDesc = "" + "\n" + "request_id\x18\x03 \x01(\tH\x01R\trequestId\x88\x01\x01B\b\n" + "\x06_quotaB\r\n" + + "\v_request_id\"\xda\x02\n" + + "\x18CodexAuthStartedResponse\x12)\n" + + "\x10verification_uri\x18\x01 \x01(\tR\x0fverificationUri\x12?\n" + + "\x19verification_uri_complete\x18\x02 \x01(\tH\x00R\x17verificationUriComplete\x88\x01\x01\x12\x1b\n" + + "\tuser_code\x18\x03 \x01(\tR\buserCode\x12)\n" + + "\x10interval_seconds\x18\x04 \x01(\x05R\x0fintervalSeconds\x129\n" + + "\n" + + "expires_at\x18\x05 \x01(\v2\x1a.google.protobuf.TimestampR\texpiresAt\x12\"\n" + + "\n" + + "request_id\x18\x06 \x01(\tH\x01R\trequestId\x88\x01\x01B\x1c\n" + + "\x1a_verification_uri_completeB\r\n" + + "\v_request_id\"\xa6\x02\n" + + "\x17CodexAuthStatusResponse\x121\n" + + "\x05state\x18\x01 \x01(\x0e2\x1b.netclode.v1.CodexAuthStateR\x05state\x12\"\n" + + "\n" + + "account_id\x18\x02 \x01(\tH\x00R\taccountId\x88\x01\x01\x12>\n" + + "\n" + + "expires_at\x18\x03 \x01(\v2\x1a.google.protobuf.TimestampH\x01R\texpiresAt\x88\x01\x01\x12\x19\n" + + "\x05error\x18\x04 \x01(\tH\x02R\x05error\x88\x01\x01\x12\"\n" + + "\n" + + "request_id\x18\x05 \x01(\tH\x03R\trequestId\x88\x01\x01B\r\n" + + "\v_account_idB\r\n" + + "\v_expires_atB\b\n" + + "\x06_errorB\r\n" + + "\v_request_id\"O\n" + + "\x1aCodexAuthLoggedOutResponse\x12\"\n" + + "\n" + + "request_id\x18\x01 \x01(\tH\x00R\trequestId\x88\x01\x01B\r\n" + "\v_request_id\"k\n" + "\x17SnapshotCreatedResponse\x12\x1d\n" + "\n" + @@ -3627,7 +4426,13 @@ const file_netclode_v1_client_proto_rawDesc = "" + "\x11default_memory_mb\x18\x04 \x01(\x05R\x0fdefaultMemoryMb\x12\"\n" + "\n" + "request_id\x18\x05 \x01(\tH\x00R\trequestId\x88\x01\x01B\r\n" + - "\v_request_id2V\n" + + "\v_request_id*\xae\x01\n" + + "\x0eCodexAuthState\x12 \n" + + "\x1cCODEX_AUTH_STATE_UNSPECIFIED\x10\x00\x12$\n" + + " CODEX_AUTH_STATE_UNAUTHENTICATED\x10\x01\x12\x1c\n" + + "\x18CODEX_AUTH_STATE_PENDING\x10\x02\x12\x1a\n" + + "\x16CODEX_AUTH_STATE_READY\x10\x03\x12\x1a\n" + + "\x16CODEX_AUTH_STATE_ERROR\x10\x042V\n" + "\rClientService\x12E\n" + "\aConnect\x12\x1a.netclode.v1.ClientMessage\x1a\x1a.netclode.v1.ServerMessage(\x010\x01B\xbc\x01\n" + "\x0fcom.netclode.v1B\vClientProtoP\x01ZOgithub.com/angristan/netclode/services/control-plane/gen/netclode/v1;netclodev1\xa2\x02\x03NXX\xaa\x02\vNetclode.V1\xca\x02\vNetclode\\V1\xe2\x02\x17Netclode\\V1\\GPBMetadata\xea\x02\fNetclode::V1b\x06proto3" @@ -3644,147 +4449,171 @@ func file_netclode_v1_client_proto_rawDescGZIP() []byte { return file_netclode_v1_client_proto_rawDescData } -var file_netclode_v1_client_proto_msgTypes = make([]protoimpl.MessageInfo, 45) +var file_netclode_v1_client_proto_enumTypes = make([]protoimpl.EnumInfo, 1) +var file_netclode_v1_client_proto_msgTypes = make([]protoimpl.MessageInfo, 54) var file_netclode_v1_client_proto_goTypes = []any{ - (*ClientMessage)(nil), // 0: netclode.v1.ClientMessage - (*ServerMessage)(nil), // 1: netclode.v1.ServerMessage - (*NetworkConfig)(nil), // 2: netclode.v1.NetworkConfig - (*CreateSessionRequest)(nil), // 3: netclode.v1.CreateSessionRequest - (*ListSessionsRequest)(nil), // 4: netclode.v1.ListSessionsRequest - (*OpenSessionRequest)(nil), // 5: netclode.v1.OpenSessionRequest - (*ResumeSessionRequest)(nil), // 6: netclode.v1.ResumeSessionRequest - (*PauseSessionRequest)(nil), // 7: netclode.v1.PauseSessionRequest - (*DeleteSessionRequest)(nil), // 8: netclode.v1.DeleteSessionRequest - (*DeleteAllSessionsRequest)(nil), // 9: netclode.v1.DeleteAllSessionsRequest - (*SendPromptRequest)(nil), // 10: netclode.v1.SendPromptRequest - (*InterruptPromptRequest)(nil), // 11: netclode.v1.InterruptPromptRequest - (*TerminalInputRequest)(nil), // 12: netclode.v1.TerminalInputRequest - (*TerminalResizeRequest)(nil), // 13: netclode.v1.TerminalResizeRequest - (*ExposePortRequest)(nil), // 14: netclode.v1.ExposePortRequest - (*SyncRequest)(nil), // 15: netclode.v1.SyncRequest - (*ListGitHubReposRequest)(nil), // 16: netclode.v1.ListGitHubReposRequest - (*GitStatusRequest)(nil), // 17: netclode.v1.GitStatusRequest - (*GitDiffRequest)(nil), // 18: netclode.v1.GitDiffRequest - (*ListModelsRequest)(nil), // 19: netclode.v1.ListModelsRequest - (*GetCopilotStatusRequest)(nil), // 20: netclode.v1.GetCopilotStatusRequest - (*ListSnapshotsRequest)(nil), // 21: netclode.v1.ListSnapshotsRequest - (*RestoreSnapshotRequest)(nil), // 22: netclode.v1.RestoreSnapshotRequest - (*UpdateRepoAccessRequest)(nil), // 23: netclode.v1.UpdateRepoAccessRequest - (*GetResourceLimitsRequest)(nil), // 24: netclode.v1.GetResourceLimitsRequest - (*SessionCreatedResponse)(nil), // 25: netclode.v1.SessionCreatedResponse - (*SessionUpdatedResponse)(nil), // 26: netclode.v1.SessionUpdatedResponse - (*SessionDeletedResponse)(nil), // 27: netclode.v1.SessionDeletedResponse - (*SessionsDeletedAllResponse)(nil), // 28: netclode.v1.SessionsDeletedAllResponse - (*SessionListResponse)(nil), // 29: netclode.v1.SessionListResponse - (*SessionStateResponse)(nil), // 30: netclode.v1.SessionStateResponse - (*SyncResponse)(nil), // 31: netclode.v1.SyncResponse - (*StreamEntryResponse)(nil), // 32: netclode.v1.StreamEntryResponse - (*PortExposedResponse)(nil), // 33: netclode.v1.PortExposedResponse - (*GitHubReposResponse)(nil), // 34: netclode.v1.GitHubReposResponse - (*GitStatusResponse)(nil), // 35: netclode.v1.GitStatusResponse - (*GitDiffResponse)(nil), // 36: netclode.v1.GitDiffResponse - (*ErrorResponse)(nil), // 37: netclode.v1.ErrorResponse - (*ModelsResponse)(nil), // 38: netclode.v1.ModelsResponse - (*CopilotStatusResponse)(nil), // 39: netclode.v1.CopilotStatusResponse - (*SnapshotCreatedResponse)(nil), // 40: netclode.v1.SnapshotCreatedResponse - (*SnapshotListResponse)(nil), // 41: netclode.v1.SnapshotListResponse - (*SnapshotRestoredResponse)(nil), // 42: netclode.v1.SnapshotRestoredResponse - (*RepoAccessUpdatedResponse)(nil), // 43: netclode.v1.RepoAccessUpdatedResponse - (*ResourceLimitsResponse)(nil), // 44: netclode.v1.ResourceLimitsResponse - (RepoAccess)(0), // 45: netclode.v1.RepoAccess - (SdkType)(0), // 46: netclode.v1.SdkType - (CopilotBackend)(0), // 47: netclode.v1.CopilotBackend - (*SandboxResources)(nil), // 48: netclode.v1.SandboxResources - (*Session)(nil), // 49: netclode.v1.Session - (*StreamEntry)(nil), // 50: netclode.v1.StreamEntry - (*InProgressState)(nil), // 51: netclode.v1.InProgressState - (*SessionSummary)(nil), // 52: netclode.v1.SessionSummary - (*timestamppb.Timestamp)(nil), // 53: google.protobuf.Timestamp - (*GitHubRepo)(nil), // 54: netclode.v1.GitHubRepo - (*GitFileChange)(nil), // 55: netclode.v1.GitFileChange - (*Error)(nil), // 56: netclode.v1.Error - (*ModelInfo)(nil), // 57: netclode.v1.ModelInfo - (*CopilotAuthStatus)(nil), // 58: netclode.v1.CopilotAuthStatus - (*CopilotPremiumQuota)(nil), // 59: netclode.v1.CopilotPremiumQuota - (*Snapshot)(nil), // 60: netclode.v1.Snapshot + (CodexAuthState)(0), // 0: netclode.v1.CodexAuthState + (*ClientMessage)(nil), // 1: netclode.v1.ClientMessage + (*ServerMessage)(nil), // 2: netclode.v1.ServerMessage + (*NetworkConfig)(nil), // 3: netclode.v1.NetworkConfig + (*CodexOAuthTokens)(nil), // 4: netclode.v1.CodexOAuthTokens + (*CreateSessionRequest)(nil), // 5: netclode.v1.CreateSessionRequest + (*ListSessionsRequest)(nil), // 6: netclode.v1.ListSessionsRequest + (*OpenSessionRequest)(nil), // 7: netclode.v1.OpenSessionRequest + (*ResumeSessionRequest)(nil), // 8: netclode.v1.ResumeSessionRequest + (*PauseSessionRequest)(nil), // 9: netclode.v1.PauseSessionRequest + (*DeleteSessionRequest)(nil), // 10: netclode.v1.DeleteSessionRequest + (*DeleteAllSessionsRequest)(nil), // 11: netclode.v1.DeleteAllSessionsRequest + (*SendPromptRequest)(nil), // 12: netclode.v1.SendPromptRequest + (*InterruptPromptRequest)(nil), // 13: netclode.v1.InterruptPromptRequest + (*TerminalInputRequest)(nil), // 14: netclode.v1.TerminalInputRequest + (*TerminalResizeRequest)(nil), // 15: netclode.v1.TerminalResizeRequest + (*ExposePortRequest)(nil), // 16: netclode.v1.ExposePortRequest + (*UnexposePortRequest)(nil), // 17: netclode.v1.UnexposePortRequest + (*SyncRequest)(nil), // 18: netclode.v1.SyncRequest + (*ListGitHubReposRequest)(nil), // 19: netclode.v1.ListGitHubReposRequest + (*GitStatusRequest)(nil), // 20: netclode.v1.GitStatusRequest + (*GitDiffRequest)(nil), // 21: netclode.v1.GitDiffRequest + (*ListModelsRequest)(nil), // 22: netclode.v1.ListModelsRequest + (*GetCopilotStatusRequest)(nil), // 23: netclode.v1.GetCopilotStatusRequest + (*ListSnapshotsRequest)(nil), // 24: netclode.v1.ListSnapshotsRequest + (*RestoreSnapshotRequest)(nil), // 25: netclode.v1.RestoreSnapshotRequest + (*UpdateRepoAccessRequest)(nil), // 26: netclode.v1.UpdateRepoAccessRequest + (*GetResourceLimitsRequest)(nil), // 27: netclode.v1.GetResourceLimitsRequest + (*CodexAuthStartRequest)(nil), // 28: netclode.v1.CodexAuthStartRequest + (*CodexAuthStatusRequest)(nil), // 29: netclode.v1.CodexAuthStatusRequest + (*CodexAuthLogoutRequest)(nil), // 30: netclode.v1.CodexAuthLogoutRequest + (*SessionCreatedResponse)(nil), // 31: netclode.v1.SessionCreatedResponse + (*SessionUpdatedResponse)(nil), // 32: netclode.v1.SessionUpdatedResponse + (*SessionDeletedResponse)(nil), // 33: netclode.v1.SessionDeletedResponse + (*SessionsDeletedAllResponse)(nil), // 34: netclode.v1.SessionsDeletedAllResponse + (*SessionListResponse)(nil), // 35: netclode.v1.SessionListResponse + (*SessionStateResponse)(nil), // 36: netclode.v1.SessionStateResponse + (*SyncResponse)(nil), // 37: netclode.v1.SyncResponse + (*StreamEntryResponse)(nil), // 38: netclode.v1.StreamEntryResponse + (*PortExposedResponse)(nil), // 39: netclode.v1.PortExposedResponse + (*PortUnexposedResponse)(nil), // 40: netclode.v1.PortUnexposedResponse + (*GitHubReposResponse)(nil), // 41: netclode.v1.GitHubReposResponse + (*GitStatusResponse)(nil), // 42: netclode.v1.GitStatusResponse + (*GitDiffResponse)(nil), // 43: netclode.v1.GitDiffResponse + (*ErrorResponse)(nil), // 44: netclode.v1.ErrorResponse + (*ModelsResponse)(nil), // 45: netclode.v1.ModelsResponse + (*CopilotStatusResponse)(nil), // 46: netclode.v1.CopilotStatusResponse + (*CodexAuthStartedResponse)(nil), // 47: netclode.v1.CodexAuthStartedResponse + (*CodexAuthStatusResponse)(nil), // 48: netclode.v1.CodexAuthStatusResponse + (*CodexAuthLoggedOutResponse)(nil), // 49: netclode.v1.CodexAuthLoggedOutResponse + (*SnapshotCreatedResponse)(nil), // 50: netclode.v1.SnapshotCreatedResponse + (*SnapshotListResponse)(nil), // 51: netclode.v1.SnapshotListResponse + (*SnapshotRestoredResponse)(nil), // 52: netclode.v1.SnapshotRestoredResponse + (*RepoAccessUpdatedResponse)(nil), // 53: netclode.v1.RepoAccessUpdatedResponse + (*ResourceLimitsResponse)(nil), // 54: netclode.v1.ResourceLimitsResponse + (*timestamppb.Timestamp)(nil), // 55: google.protobuf.Timestamp + (RepoAccess)(0), // 56: netclode.v1.RepoAccess + (SdkType)(0), // 57: netclode.v1.SdkType + (CopilotBackend)(0), // 58: netclode.v1.CopilotBackend + (*SandboxResources)(nil), // 59: netclode.v1.SandboxResources + (*Session)(nil), // 60: netclode.v1.Session + (*StreamEntry)(nil), // 61: netclode.v1.StreamEntry + (*InProgressState)(nil), // 62: netclode.v1.InProgressState + (*SessionSummary)(nil), // 63: netclode.v1.SessionSummary + (*GitHubRepo)(nil), // 64: netclode.v1.GitHubRepo + (*GitFileChange)(nil), // 65: netclode.v1.GitFileChange + (*Error)(nil), // 66: netclode.v1.Error + (*ModelInfo)(nil), // 67: netclode.v1.ModelInfo + (*CopilotAuthStatus)(nil), // 68: netclode.v1.CopilotAuthStatus + (*CopilotPremiumQuota)(nil), // 69: netclode.v1.CopilotPremiumQuota + (*Snapshot)(nil), // 70: netclode.v1.Snapshot } var file_netclode_v1_client_proto_depIdxs = []int32{ - 3, // 0: netclode.v1.ClientMessage.create_session:type_name -> netclode.v1.CreateSessionRequest - 4, // 1: netclode.v1.ClientMessage.list_sessions:type_name -> netclode.v1.ListSessionsRequest - 5, // 2: netclode.v1.ClientMessage.open_session:type_name -> netclode.v1.OpenSessionRequest - 6, // 3: netclode.v1.ClientMessage.resume_session:type_name -> netclode.v1.ResumeSessionRequest - 7, // 4: netclode.v1.ClientMessage.pause_session:type_name -> netclode.v1.PauseSessionRequest - 8, // 5: netclode.v1.ClientMessage.delete_session:type_name -> netclode.v1.DeleteSessionRequest - 9, // 6: netclode.v1.ClientMessage.delete_all_sessions:type_name -> netclode.v1.DeleteAllSessionsRequest - 10, // 7: netclode.v1.ClientMessage.send_prompt:type_name -> netclode.v1.SendPromptRequest - 11, // 8: netclode.v1.ClientMessage.interrupt_prompt:type_name -> netclode.v1.InterruptPromptRequest - 12, // 9: netclode.v1.ClientMessage.terminal_input:type_name -> netclode.v1.TerminalInputRequest - 13, // 10: netclode.v1.ClientMessage.terminal_resize:type_name -> netclode.v1.TerminalResizeRequest - 14, // 11: netclode.v1.ClientMessage.expose_port:type_name -> netclode.v1.ExposePortRequest - 15, // 12: netclode.v1.ClientMessage.sync:type_name -> netclode.v1.SyncRequest - 16, // 13: netclode.v1.ClientMessage.list_github_repos:type_name -> netclode.v1.ListGitHubReposRequest - 17, // 14: netclode.v1.ClientMessage.git_status:type_name -> netclode.v1.GitStatusRequest - 18, // 15: netclode.v1.ClientMessage.git_diff:type_name -> netclode.v1.GitDiffRequest - 19, // 16: netclode.v1.ClientMessage.list_models:type_name -> netclode.v1.ListModelsRequest - 20, // 17: netclode.v1.ClientMessage.get_copilot_status:type_name -> netclode.v1.GetCopilotStatusRequest - 21, // 18: netclode.v1.ClientMessage.list_snapshots:type_name -> netclode.v1.ListSnapshotsRequest - 22, // 19: netclode.v1.ClientMessage.restore_snapshot:type_name -> netclode.v1.RestoreSnapshotRequest - 23, // 20: netclode.v1.ClientMessage.update_repo_access:type_name -> netclode.v1.UpdateRepoAccessRequest - 24, // 21: netclode.v1.ClientMessage.get_resource_limits:type_name -> netclode.v1.GetResourceLimitsRequest - 25, // 22: netclode.v1.ServerMessage.session_created:type_name -> netclode.v1.SessionCreatedResponse - 26, // 23: netclode.v1.ServerMessage.session_updated:type_name -> netclode.v1.SessionUpdatedResponse - 27, // 24: netclode.v1.ServerMessage.session_deleted:type_name -> netclode.v1.SessionDeletedResponse - 28, // 25: netclode.v1.ServerMessage.sessions_deleted_all:type_name -> netclode.v1.SessionsDeletedAllResponse - 29, // 26: netclode.v1.ServerMessage.session_list:type_name -> netclode.v1.SessionListResponse - 30, // 27: netclode.v1.ServerMessage.session_state:type_name -> netclode.v1.SessionStateResponse - 31, // 28: netclode.v1.ServerMessage.sync_response:type_name -> netclode.v1.SyncResponse - 32, // 29: netclode.v1.ServerMessage.stream_entry:type_name -> netclode.v1.StreamEntryResponse - 33, // 30: netclode.v1.ServerMessage.port_exposed:type_name -> netclode.v1.PortExposedResponse - 34, // 31: netclode.v1.ServerMessage.github_repos:type_name -> netclode.v1.GitHubReposResponse - 35, // 32: netclode.v1.ServerMessage.git_status:type_name -> netclode.v1.GitStatusResponse - 36, // 33: netclode.v1.ServerMessage.git_diff:type_name -> netclode.v1.GitDiffResponse - 37, // 34: netclode.v1.ServerMessage.error:type_name -> netclode.v1.ErrorResponse - 38, // 35: netclode.v1.ServerMessage.models:type_name -> netclode.v1.ModelsResponse - 39, // 36: netclode.v1.ServerMessage.copilot_status:type_name -> netclode.v1.CopilotStatusResponse - 40, // 37: netclode.v1.ServerMessage.snapshot_created:type_name -> netclode.v1.SnapshotCreatedResponse - 41, // 38: netclode.v1.ServerMessage.snapshot_list:type_name -> netclode.v1.SnapshotListResponse - 42, // 39: netclode.v1.ServerMessage.snapshot_restored:type_name -> netclode.v1.SnapshotRestoredResponse - 43, // 40: netclode.v1.ServerMessage.repo_access_updated:type_name -> netclode.v1.RepoAccessUpdatedResponse - 44, // 41: netclode.v1.ServerMessage.resource_limits:type_name -> netclode.v1.ResourceLimitsResponse - 45, // 42: netclode.v1.CreateSessionRequest.repo_access:type_name -> netclode.v1.RepoAccess - 46, // 43: netclode.v1.CreateSessionRequest.sdk_type:type_name -> netclode.v1.SdkType - 47, // 44: netclode.v1.CreateSessionRequest.copilot_backend:type_name -> netclode.v1.CopilotBackend - 2, // 45: netclode.v1.CreateSessionRequest.network_config:type_name -> netclode.v1.NetworkConfig - 48, // 46: netclode.v1.CreateSessionRequest.resources:type_name -> netclode.v1.SandboxResources - 46, // 47: netclode.v1.ListModelsRequest.sdk_type:type_name -> netclode.v1.SdkType - 47, // 48: netclode.v1.ListModelsRequest.copilot_backend:type_name -> netclode.v1.CopilotBackend - 45, // 49: netclode.v1.UpdateRepoAccessRequest.repo_access:type_name -> netclode.v1.RepoAccess - 49, // 50: netclode.v1.SessionCreatedResponse.session:type_name -> netclode.v1.Session - 49, // 51: netclode.v1.SessionUpdatedResponse.session:type_name -> netclode.v1.Session - 49, // 52: netclode.v1.SessionListResponse.sessions:type_name -> netclode.v1.Session - 49, // 53: netclode.v1.SessionStateResponse.session:type_name -> netclode.v1.Session - 50, // 54: netclode.v1.SessionStateResponse.entries:type_name -> netclode.v1.StreamEntry - 51, // 55: netclode.v1.SessionStateResponse.in_progress:type_name -> netclode.v1.InProgressState - 52, // 56: netclode.v1.SyncResponse.sessions:type_name -> netclode.v1.SessionSummary - 53, // 57: netclode.v1.SyncResponse.server_time:type_name -> google.protobuf.Timestamp - 50, // 58: netclode.v1.StreamEntryResponse.entry:type_name -> netclode.v1.StreamEntry - 54, // 59: netclode.v1.GitHubReposResponse.repos:type_name -> netclode.v1.GitHubRepo - 55, // 60: netclode.v1.GitStatusResponse.files:type_name -> netclode.v1.GitFileChange - 56, // 61: netclode.v1.ErrorResponse.error:type_name -> netclode.v1.Error - 57, // 62: netclode.v1.ModelsResponse.models:type_name -> netclode.v1.ModelInfo - 46, // 63: netclode.v1.ModelsResponse.sdk_type:type_name -> netclode.v1.SdkType - 58, // 64: netclode.v1.CopilotStatusResponse.auth:type_name -> netclode.v1.CopilotAuthStatus - 59, // 65: netclode.v1.CopilotStatusResponse.quota:type_name -> netclode.v1.CopilotPremiumQuota - 60, // 66: netclode.v1.SnapshotCreatedResponse.snapshot:type_name -> netclode.v1.Snapshot - 60, // 67: netclode.v1.SnapshotListResponse.snapshots:type_name -> netclode.v1.Snapshot - 45, // 68: netclode.v1.RepoAccessUpdatedResponse.repo_access:type_name -> netclode.v1.RepoAccess - 0, // 69: netclode.v1.ClientService.Connect:input_type -> netclode.v1.ClientMessage - 1, // 70: netclode.v1.ClientService.Connect:output_type -> netclode.v1.ServerMessage - 70, // [70:71] is the sub-list for method output_type - 69, // [69:70] is the sub-list for method input_type - 69, // [69:69] is the sub-list for extension type_name - 69, // [69:69] is the sub-list for extension extendee - 0, // [0:69] is the sub-list for field type_name + 5, // 0: netclode.v1.ClientMessage.create_session:type_name -> netclode.v1.CreateSessionRequest + 6, // 1: netclode.v1.ClientMessage.list_sessions:type_name -> netclode.v1.ListSessionsRequest + 7, // 2: netclode.v1.ClientMessage.open_session:type_name -> netclode.v1.OpenSessionRequest + 8, // 3: netclode.v1.ClientMessage.resume_session:type_name -> netclode.v1.ResumeSessionRequest + 9, // 4: netclode.v1.ClientMessage.pause_session:type_name -> netclode.v1.PauseSessionRequest + 10, // 5: netclode.v1.ClientMessage.delete_session:type_name -> netclode.v1.DeleteSessionRequest + 11, // 6: netclode.v1.ClientMessage.delete_all_sessions:type_name -> netclode.v1.DeleteAllSessionsRequest + 12, // 7: netclode.v1.ClientMessage.send_prompt:type_name -> netclode.v1.SendPromptRequest + 13, // 8: netclode.v1.ClientMessage.interrupt_prompt:type_name -> netclode.v1.InterruptPromptRequest + 14, // 9: netclode.v1.ClientMessage.terminal_input:type_name -> netclode.v1.TerminalInputRequest + 15, // 10: netclode.v1.ClientMessage.terminal_resize:type_name -> netclode.v1.TerminalResizeRequest + 16, // 11: netclode.v1.ClientMessage.expose_port:type_name -> netclode.v1.ExposePortRequest + 18, // 12: netclode.v1.ClientMessage.sync:type_name -> netclode.v1.SyncRequest + 19, // 13: netclode.v1.ClientMessage.list_github_repos:type_name -> netclode.v1.ListGitHubReposRequest + 20, // 14: netclode.v1.ClientMessage.git_status:type_name -> netclode.v1.GitStatusRequest + 21, // 15: netclode.v1.ClientMessage.git_diff:type_name -> netclode.v1.GitDiffRequest + 22, // 16: netclode.v1.ClientMessage.list_models:type_name -> netclode.v1.ListModelsRequest + 23, // 17: netclode.v1.ClientMessage.get_copilot_status:type_name -> netclode.v1.GetCopilotStatusRequest + 24, // 18: netclode.v1.ClientMessage.list_snapshots:type_name -> netclode.v1.ListSnapshotsRequest + 25, // 19: netclode.v1.ClientMessage.restore_snapshot:type_name -> netclode.v1.RestoreSnapshotRequest + 26, // 20: netclode.v1.ClientMessage.update_repo_access:type_name -> netclode.v1.UpdateRepoAccessRequest + 27, // 21: netclode.v1.ClientMessage.get_resource_limits:type_name -> netclode.v1.GetResourceLimitsRequest + 28, // 22: netclode.v1.ClientMessage.codex_auth_start:type_name -> netclode.v1.CodexAuthStartRequest + 29, // 23: netclode.v1.ClientMessage.codex_auth_status:type_name -> netclode.v1.CodexAuthStatusRequest + 30, // 24: netclode.v1.ClientMessage.codex_auth_logout:type_name -> netclode.v1.CodexAuthLogoutRequest + 17, // 25: netclode.v1.ClientMessage.unexpose_port:type_name -> netclode.v1.UnexposePortRequest + 31, // 26: netclode.v1.ServerMessage.session_created:type_name -> netclode.v1.SessionCreatedResponse + 32, // 27: netclode.v1.ServerMessage.session_updated:type_name -> netclode.v1.SessionUpdatedResponse + 33, // 28: netclode.v1.ServerMessage.session_deleted:type_name -> netclode.v1.SessionDeletedResponse + 34, // 29: netclode.v1.ServerMessage.sessions_deleted_all:type_name -> netclode.v1.SessionsDeletedAllResponse + 35, // 30: netclode.v1.ServerMessage.session_list:type_name -> netclode.v1.SessionListResponse + 36, // 31: netclode.v1.ServerMessage.session_state:type_name -> netclode.v1.SessionStateResponse + 37, // 32: netclode.v1.ServerMessage.sync_response:type_name -> netclode.v1.SyncResponse + 38, // 33: netclode.v1.ServerMessage.stream_entry:type_name -> netclode.v1.StreamEntryResponse + 39, // 34: netclode.v1.ServerMessage.port_exposed:type_name -> netclode.v1.PortExposedResponse + 41, // 35: netclode.v1.ServerMessage.github_repos:type_name -> netclode.v1.GitHubReposResponse + 42, // 36: netclode.v1.ServerMessage.git_status:type_name -> netclode.v1.GitStatusResponse + 43, // 37: netclode.v1.ServerMessage.git_diff:type_name -> netclode.v1.GitDiffResponse + 44, // 38: netclode.v1.ServerMessage.error:type_name -> netclode.v1.ErrorResponse + 45, // 39: netclode.v1.ServerMessage.models:type_name -> netclode.v1.ModelsResponse + 46, // 40: netclode.v1.ServerMessage.copilot_status:type_name -> netclode.v1.CopilotStatusResponse + 50, // 41: netclode.v1.ServerMessage.snapshot_created:type_name -> netclode.v1.SnapshotCreatedResponse + 51, // 42: netclode.v1.ServerMessage.snapshot_list:type_name -> netclode.v1.SnapshotListResponse + 52, // 43: netclode.v1.ServerMessage.snapshot_restored:type_name -> netclode.v1.SnapshotRestoredResponse + 53, // 44: netclode.v1.ServerMessage.repo_access_updated:type_name -> netclode.v1.RepoAccessUpdatedResponse + 54, // 45: netclode.v1.ServerMessage.resource_limits:type_name -> netclode.v1.ResourceLimitsResponse + 47, // 46: netclode.v1.ServerMessage.codex_auth_started:type_name -> netclode.v1.CodexAuthStartedResponse + 48, // 47: netclode.v1.ServerMessage.codex_auth_status:type_name -> netclode.v1.CodexAuthStatusResponse + 49, // 48: netclode.v1.ServerMessage.codex_auth_logged_out:type_name -> netclode.v1.CodexAuthLoggedOutResponse + 40, // 49: netclode.v1.ServerMessage.port_unexposed:type_name -> netclode.v1.PortUnexposedResponse + 55, // 50: netclode.v1.CodexOAuthTokens.expires_at:type_name -> google.protobuf.Timestamp + 56, // 51: netclode.v1.CreateSessionRequest.repo_access:type_name -> netclode.v1.RepoAccess + 57, // 52: netclode.v1.CreateSessionRequest.sdk_type:type_name -> netclode.v1.SdkType + 58, // 53: netclode.v1.CreateSessionRequest.copilot_backend:type_name -> netclode.v1.CopilotBackend + 3, // 54: netclode.v1.CreateSessionRequest.network_config:type_name -> netclode.v1.NetworkConfig + 59, // 55: netclode.v1.CreateSessionRequest.resources:type_name -> netclode.v1.SandboxResources + 4, // 56: netclode.v1.CreateSessionRequest.codex_oauth_tokens:type_name -> netclode.v1.CodexOAuthTokens + 57, // 57: netclode.v1.ListModelsRequest.sdk_type:type_name -> netclode.v1.SdkType + 58, // 58: netclode.v1.ListModelsRequest.copilot_backend:type_name -> netclode.v1.CopilotBackend + 56, // 59: netclode.v1.UpdateRepoAccessRequest.repo_access:type_name -> netclode.v1.RepoAccess + 60, // 60: netclode.v1.SessionCreatedResponse.session:type_name -> netclode.v1.Session + 60, // 61: netclode.v1.SessionUpdatedResponse.session:type_name -> netclode.v1.Session + 60, // 62: netclode.v1.SessionListResponse.sessions:type_name -> netclode.v1.Session + 60, // 63: netclode.v1.SessionStateResponse.session:type_name -> netclode.v1.Session + 61, // 64: netclode.v1.SessionStateResponse.entries:type_name -> netclode.v1.StreamEntry + 62, // 65: netclode.v1.SessionStateResponse.in_progress:type_name -> netclode.v1.InProgressState + 63, // 66: netclode.v1.SyncResponse.sessions:type_name -> netclode.v1.SessionSummary + 55, // 67: netclode.v1.SyncResponse.server_time:type_name -> google.protobuf.Timestamp + 61, // 68: netclode.v1.StreamEntryResponse.entry:type_name -> netclode.v1.StreamEntry + 64, // 69: netclode.v1.GitHubReposResponse.repos:type_name -> netclode.v1.GitHubRepo + 65, // 70: netclode.v1.GitStatusResponse.files:type_name -> netclode.v1.GitFileChange + 66, // 71: netclode.v1.ErrorResponse.error:type_name -> netclode.v1.Error + 67, // 72: netclode.v1.ModelsResponse.models:type_name -> netclode.v1.ModelInfo + 57, // 73: netclode.v1.ModelsResponse.sdk_type:type_name -> netclode.v1.SdkType + 68, // 74: netclode.v1.CopilotStatusResponse.auth:type_name -> netclode.v1.CopilotAuthStatus + 69, // 75: netclode.v1.CopilotStatusResponse.quota:type_name -> netclode.v1.CopilotPremiumQuota + 55, // 76: netclode.v1.CodexAuthStartedResponse.expires_at:type_name -> google.protobuf.Timestamp + 0, // 77: netclode.v1.CodexAuthStatusResponse.state:type_name -> netclode.v1.CodexAuthState + 55, // 78: netclode.v1.CodexAuthStatusResponse.expires_at:type_name -> google.protobuf.Timestamp + 70, // 79: netclode.v1.SnapshotCreatedResponse.snapshot:type_name -> netclode.v1.Snapshot + 70, // 80: netclode.v1.SnapshotListResponse.snapshots:type_name -> netclode.v1.Snapshot + 56, // 81: netclode.v1.RepoAccessUpdatedResponse.repo_access:type_name -> netclode.v1.RepoAccess + 1, // 82: netclode.v1.ClientService.Connect:input_type -> netclode.v1.ClientMessage + 2, // 83: netclode.v1.ClientService.Connect:output_type -> netclode.v1.ServerMessage + 83, // [83:84] is the sub-list for method output_type + 82, // [82:83] is the sub-list for method input_type + 82, // [82:82] is the sub-list for extension type_name + 82, // [82:82] is the sub-list for extension extendee + 0, // [0:82] is the sub-list for field type_name } func init() { file_netclode_v1_client_proto_init() } @@ -3817,6 +4646,10 @@ func file_netclode_v1_client_proto_init() { (*ClientMessage_RestoreSnapshot)(nil), (*ClientMessage_UpdateRepoAccess)(nil), (*ClientMessage_GetResourceLimits)(nil), + (*ClientMessage_CodexAuthStart)(nil), + (*ClientMessage_CodexAuthStatus)(nil), + (*ClientMessage_CodexAuthLogout)(nil), + (*ClientMessage_UnexposePort)(nil), } file_netclode_v1_client_proto_msgTypes[1].OneofWrappers = []any{ (*ServerMessage_SessionCreated)(nil), @@ -3839,6 +4672,10 @@ func file_netclode_v1_client_proto_init() { (*ServerMessage_SnapshotRestored)(nil), (*ServerMessage_RepoAccessUpdated)(nil), (*ServerMessage_ResourceLimits)(nil), + (*ServerMessage_CodexAuthStarted)(nil), + (*ServerMessage_CodexAuthStatus)(nil), + (*ServerMessage_CodexAuthLoggedOut)(nil), + (*ServerMessage_PortUnexposed)(nil), } file_netclode_v1_client_proto_msgTypes[3].OneofWrappers = []any{} file_netclode_v1_client_proto_msgTypes[4].OneofWrappers = []any{} @@ -3863,34 +4700,44 @@ func file_netclode_v1_client_proto_init() { file_netclode_v1_client_proto_msgTypes[23].OneofWrappers = []any{} file_netclode_v1_client_proto_msgTypes[24].OneofWrappers = []any{} file_netclode_v1_client_proto_msgTypes[25].OneofWrappers = []any{} + file_netclode_v1_client_proto_msgTypes[26].OneofWrappers = []any{} file_netclode_v1_client_proto_msgTypes[27].OneofWrappers = []any{} file_netclode_v1_client_proto_msgTypes[28].OneofWrappers = []any{} file_netclode_v1_client_proto_msgTypes[29].OneofWrappers = []any{} file_netclode_v1_client_proto_msgTypes[30].OneofWrappers = []any{} - file_netclode_v1_client_proto_msgTypes[31].OneofWrappers = []any{} + file_netclode_v1_client_proto_msgTypes[32].OneofWrappers = []any{} file_netclode_v1_client_proto_msgTypes[33].OneofWrappers = []any{} file_netclode_v1_client_proto_msgTypes[34].OneofWrappers = []any{} file_netclode_v1_client_proto_msgTypes[35].OneofWrappers = []any{} file_netclode_v1_client_proto_msgTypes[36].OneofWrappers = []any{} - file_netclode_v1_client_proto_msgTypes[37].OneofWrappers = []any{} file_netclode_v1_client_proto_msgTypes[38].OneofWrappers = []any{} file_netclode_v1_client_proto_msgTypes[39].OneofWrappers = []any{} + file_netclode_v1_client_proto_msgTypes[40].OneofWrappers = []any{} file_netclode_v1_client_proto_msgTypes[41].OneofWrappers = []any{} file_netclode_v1_client_proto_msgTypes[42].OneofWrappers = []any{} file_netclode_v1_client_proto_msgTypes[43].OneofWrappers = []any{} file_netclode_v1_client_proto_msgTypes[44].OneofWrappers = []any{} + file_netclode_v1_client_proto_msgTypes[45].OneofWrappers = []any{} + file_netclode_v1_client_proto_msgTypes[46].OneofWrappers = []any{} + file_netclode_v1_client_proto_msgTypes[47].OneofWrappers = []any{} + file_netclode_v1_client_proto_msgTypes[48].OneofWrappers = []any{} + file_netclode_v1_client_proto_msgTypes[50].OneofWrappers = []any{} + file_netclode_v1_client_proto_msgTypes[51].OneofWrappers = []any{} + file_netclode_v1_client_proto_msgTypes[52].OneofWrappers = []any{} + file_netclode_v1_client_proto_msgTypes[53].OneofWrappers = []any{} type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_netclode_v1_client_proto_rawDesc), len(file_netclode_v1_client_proto_rawDesc)), - NumEnums: 0, - NumMessages: 45, + NumEnums: 1, + NumMessages: 54, NumExtensions: 0, NumServices: 1, }, GoTypes: file_netclode_v1_client_proto_goTypes, DependencyIndexes: file_netclode_v1_client_proto_depIdxs, + EnumInfos: file_netclode_v1_client_proto_enumTypes, MessageInfos: file_netclode_v1_client_proto_msgTypes, }.Build() File_netclode_v1_client_proto = out.File diff --git a/services/control-plane/gen/netclode/v1/client_grpc.pb.go b/services/control-plane/gen/netclode/v1/client_grpc.pb.go index 2c47b7b8..1aabbfb9 100644 --- a/services/control-plane/gen/netclode/v1/client_grpc.pb.go +++ b/services/control-plane/gen/netclode/v1/client_grpc.pb.go @@ -1,6 +1,6 @@ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: -// - protoc-gen-go-grpc v1.6.0 +// - protoc-gen-go-grpc v1.6.1 // - protoc (unknown) // source: netclode/v1/client.proto diff --git a/services/control-plane/gen/netclode/v1/common.pb.go b/services/control-plane/gen/netclode/v1/common.pb.go index 7959d069..103e845d 100644 --- a/services/control-plane/gen/netclode/v1/common.pb.go +++ b/services/control-plane/gen/netclode/v1/common.pb.go @@ -505,7 +505,6 @@ type SessionConfig struct { CodexAccessToken *string `protobuf:"bytes,11,opt,name=codex_access_token,json=codexAccessToken,proto3,oneof" json:"codex_access_token,omitempty"` CodexIdToken *string `protobuf:"bytes,12,opt,name=codex_id_token,json=codexIdToken,proto3,oneof" json:"codex_id_token,omitempty"` OpenaiApiKey *string `protobuf:"bytes,13,opt,name=openai_api_key,json=openaiApiKey,proto3,oneof" json:"openai_api_key,omitempty"` - CodexRefreshToken *string `protobuf:"bytes,14,opt,name=codex_refresh_token,json=codexRefreshToken,proto3,oneof" json:"codex_refresh_token,omitempty"` ReasoningEffort *string `protobuf:"bytes,15,opt,name=reasoning_effort,json=reasoningEffort,proto3,oneof" json:"reasoning_effort,omitempty"` MistralApiKey *string `protobuf:"bytes,16,opt,name=mistral_api_key,json=mistralApiKey,proto3,oneof" json:"mistral_api_key,omitempty"` OllamaUrl *string `protobuf:"bytes,17,opt,name=ollama_url,json=ollamaUrl,proto3,oneof" json:"ollama_url,omitempty"` // URL for local Ollama inference (e.g., "http://ollama.netclode.svc.cluster.local:11434") @@ -636,13 +635,6 @@ func (x *SessionConfig) GetOpenaiApiKey() string { return "" } -func (x *SessionConfig) GetCodexRefreshToken() string { - if x != nil && x.CodexRefreshToken != nil { - return *x.CodexRefreshToken - } - return "" -} - func (x *SessionConfig) GetReasoningEffort() string { if x != nil && x.ReasoningEffort != nil { return *x.ReasoningEffort @@ -1622,7 +1614,7 @@ const file_netclode_v1_common_proto_rawDesc = "" + "\rmessage_count\x18\x02 \x01(\x05H\x00R\fmessageCount\x88\x01\x01\x12)\n" + "\x0elast_stream_id\x18\x03 \x01(\tH\x01R\flastStreamId\x88\x01\x01B\x10\n" + "\x0e_message_countB\x11\n" + - "\x0f_last_stream_id\"\xf9\b\n" + + "\x0f_last_stream_id\"\xac\b\n" + "\rSessionConfig\x12\x1d\n" + "\n" + "session_id\x18\x01 \x01(\tR\tsessionId\x12#\n" + @@ -1639,15 +1631,14 @@ const file_netclode_v1_common_proto_rawDesc = "" + " \x01(\tH\x05R\x12githubCopilotToken\x88\x01\x01\x121\n" + "\x12codex_access_token\x18\v \x01(\tH\x06R\x10codexAccessToken\x88\x01\x01\x12)\n" + "\x0ecodex_id_token\x18\f \x01(\tH\aR\fcodexIdToken\x88\x01\x01\x12)\n" + - "\x0eopenai_api_key\x18\r \x01(\tH\bR\fopenaiApiKey\x88\x01\x01\x123\n" + - "\x13codex_refresh_token\x18\x0e \x01(\tH\tR\x11codexRefreshToken\x88\x01\x01\x12.\n" + - "\x10reasoning_effort\x18\x0f \x01(\tH\n" + - "R\x0freasoningEffort\x88\x01\x01\x12+\n" + - "\x0fmistral_api_key\x18\x10 \x01(\tH\vR\rmistralApiKey\x88\x01\x01\x12\"\n" + + "\x0eopenai_api_key\x18\r \x01(\tH\bR\fopenaiApiKey\x88\x01\x01\x12.\n" + + "\x10reasoning_effort\x18\x0f \x01(\tH\tR\x0freasoningEffort\x88\x01\x01\x12+\n" + + "\x0fmistral_api_key\x18\x10 \x01(\tH\n" + + "R\rmistralApiKey\x88\x01\x01\x12\"\n" + "\n" + - "ollama_url\x18\x11 \x01(\tH\fR\tollamaUrl\x88\x01\x01\x12-\n" + - "\x10opencode_api_key\x18\x12 \x01(\tH\rR\x0eopencodeApiKey\x88\x01\x01\x12#\n" + - "\vzai_api_key\x18\x13 \x01(\tH\x0eR\tzaiApiKey\x88\x01\x01B\x0f\n" + + "ollama_url\x18\x11 \x01(\tH\vR\tollamaUrl\x88\x01\x01\x12-\n" + + "\x10opencode_api_key\x18\x12 \x01(\tH\fR\x0eopencodeApiKey\x88\x01\x01\x12#\n" + + "\vzai_api_key\x18\x13 \x01(\tH\rR\tzaiApiKey\x88\x01\x01B\x0f\n" + "\r_github_tokenB\x0e\n" + "\f_repo_accessB\v\n" + "\t_sdk_typeB\b\n" + @@ -1656,8 +1647,7 @@ const file_netclode_v1_common_proto_rawDesc = "" + "\x15_github_copilot_tokenB\x15\n" + "\x13_codex_access_tokenB\x11\n" + "\x0f_codex_id_tokenB\x11\n" + - "\x0f_openai_api_keyB\x16\n" + - "\x14_codex_refresh_tokenB\x13\n" + + "\x0f_openai_api_keyB\x13\n" + "\x11_reasoning_effortB\x12\n" + "\x10_mistral_api_keyB\r\n" + "\v_ollama_urlB\x13\n" + diff --git a/services/control-plane/gen/netclode/v1/events.pb.go b/services/control-plane/gen/netclode/v1/events.pb.go index fd83d604..89f407a1 100644 --- a/services/control-plane/gen/netclode/v1/events.pb.go +++ b/services/control-plane/gen/netclode/v1/events.pb.go @@ -40,6 +40,7 @@ const ( AgentEventKind_AGENT_EVENT_KIND_REPO_CLONE AgentEventKind = 8 // Repository clone progress AgentEventKind_AGENT_EVENT_KIND_AGENT_DISCONNECTED AgentEventKind = 9 // Agent disconnected unexpectedly AgentEventKind_AGENT_EVENT_KIND_AGENT_RECONNECTED AgentEventKind = 10 // Agent reconnected after disconnect + AgentEventKind_AGENT_EVENT_KIND_PORT_UNEXPOSED AgentEventKind = 11 // Port exposure was removed ) // Enum value maps for AgentEventKind. @@ -56,6 +57,7 @@ var ( 8: "AGENT_EVENT_KIND_REPO_CLONE", 9: "AGENT_EVENT_KIND_AGENT_DISCONNECTED", 10: "AGENT_EVENT_KIND_AGENT_RECONNECTED", + 11: "AGENT_EVENT_KIND_PORT_UNEXPOSED", } AgentEventKind_value = map[string]int32{ "AGENT_EVENT_KIND_UNSPECIFIED": 0, @@ -69,6 +71,7 @@ var ( "AGENT_EVENT_KIND_REPO_CLONE": 8, "AGENT_EVENT_KIND_AGENT_DISCONNECTED": 9, "AGENT_EVENT_KIND_AGENT_RECONNECTED": 10, + "AGENT_EVENT_KIND_PORT_UNEXPOSED": 11, } ) @@ -222,6 +225,7 @@ type AgentEvent struct { // *AgentEvent_ToolEnd // *AgentEvent_PortExposed // *AgentEvent_RepoClone + // *AgentEvent_PortUnexposed Payload isAgentEvent_Payload `protobuf_oneof:"payload"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache @@ -350,6 +354,15 @@ func (x *AgentEvent) GetRepoClone() *RepoClonePayload { return nil } +func (x *AgentEvent) GetPortUnexposed() *PortUnexposedPayload { + if x != nil { + if x, ok := x.Payload.(*AgentEvent_PortUnexposed); ok { + return x.PortUnexposed + } + } + return nil +} + type isAgentEvent_Payload interface { isAgentEvent_Payload() } @@ -386,6 +399,10 @@ type AgentEvent_RepoClone struct { RepoClone *RepoClonePayload `protobuf:"bytes,10,opt,name=repo_clone,json=repoClone,proto3,oneof"` } +type AgentEvent_PortUnexposed struct { + PortUnexposed *PortUnexposedPayload `protobuf:"bytes,11,opt,name=port_unexposed,json=portUnexposed,proto3,oneof"` +} + func (*AgentEvent_Message) isAgentEvent_Payload() {} func (*AgentEvent_Thinking) isAgentEvent_Payload() {} @@ -402,6 +419,8 @@ func (*AgentEvent_PortExposed) isAgentEvent_Payload() {} func (*AgentEvent_RepoClone) isAgentEvent_Payload() {} +func (*AgentEvent_PortUnexposed) isAgentEvent_Payload() {} + // MessagePayload contains data for user/assistant messages. type MessagePayload struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -801,6 +820,51 @@ func (x *PortExposedPayload) GetPreviewUrl() string { return "" } +// PortUnexposedPayload contains data for port removal events. +type PortUnexposedPayload struct { + state protoimpl.MessageState `protogen:"open.v1"` + Port int32 `protobuf:"varint,1,opt,name=port,proto3" json:"port,omitempty"` // The port number no longer exposed + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *PortUnexposedPayload) Reset() { + *x = PortUnexposedPayload{} + mi := &file_netclode_v1_events_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *PortUnexposedPayload) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PortUnexposedPayload) ProtoMessage() {} + +func (x *PortUnexposedPayload) ProtoReflect() protoreflect.Message { + mi := &file_netclode_v1_events_proto_msgTypes[8] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PortUnexposedPayload.ProtoReflect.Descriptor instead. +func (*PortUnexposedPayload) Descriptor() ([]byte, []int) { + return file_netclode_v1_events_proto_rawDescGZIP(), []int{8} +} + +func (x *PortUnexposedPayload) GetPort() int32 { + if x != nil { + return x.Port + } + return 0 +} + // RepoClonePayload contains data for repository clone progress events. type RepoClonePayload struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -813,7 +877,7 @@ type RepoClonePayload struct { func (x *RepoClonePayload) Reset() { *x = RepoClonePayload{} - mi := &file_netclode_v1_events_proto_msgTypes[8] + mi := &file_netclode_v1_events_proto_msgTypes[9] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -825,7 +889,7 @@ func (x *RepoClonePayload) String() string { func (*RepoClonePayload) ProtoMessage() {} func (x *RepoClonePayload) ProtoReflect() protoreflect.Message { - mi := &file_netclode_v1_events_proto_msgTypes[8] + mi := &file_netclode_v1_events_proto_msgTypes[9] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -838,7 +902,7 @@ func (x *RepoClonePayload) ProtoReflect() protoreflect.Message { // Deprecated: Use RepoClonePayload.ProtoReflect.Descriptor instead. func (*RepoClonePayload) Descriptor() ([]byte, []int) { - return file_netclode_v1_events_proto_rawDescGZIP(), []int{8} + return file_netclode_v1_events_proto_rawDescGZIP(), []int{9} } func (x *RepoClonePayload) GetRepo() string { @@ -866,7 +930,7 @@ var File_netclode_v1_events_proto protoreflect.FileDescriptor const file_netclode_v1_events_proto_rawDesc = "" + "\n" + - "\x18netclode/v1/events.proto\x12\vnetclode.v1\x1a\x1cgoogle/protobuf/struct.proto\"\xe7\x04\n" + + "\x18netclode/v1/events.proto\x12\vnetclode.v1\x1a\x1cgoogle/protobuf/struct.proto\"\xb3\x05\n" + "\n" + "AgentEvent\x12/\n" + "\x04kind\x18\x01 \x01(\x0e2\x1b.netclode.v1.AgentEventKindR\x04kind\x12%\n" + @@ -883,7 +947,8 @@ const file_netclode_v1_events_proto_rawDesc = "" + "\fport_exposed\x18\t \x01(\v2\x1f.netclode.v1.PortExposedPayloadH\x00R\vportExposed\x12>\n" + "\n" + "repo_clone\x18\n" + - " \x01(\v2\x1d.netclode.v1.RepoClonePayloadH\x00R\trepoCloneB\t\n" + + " \x01(\v2\x1d.netclode.v1.RepoClonePayloadH\x00R\trepoClone\x12J\n" + + "\x0eport_unexposed\x18\v \x01(\v2!.netclode.v1.PortUnexposedPayloadH\x00R\rportUnexposedB\t\n" + "\apayload\"X\n" + "\x0eMessagePayload\x12,\n" + "\x04role\x18\x01 \x01(\x0e2\x18.netclode.v1.MessageRoleR\x04role\x12\x18\n" + @@ -921,11 +986,13 @@ const file_netclode_v1_events_proto_rawDesc = "" + "previewUrl\x88\x01\x01B\n" + "\n" + "\b_processB\x0e\n" + - "\f_preview_url\"s\n" + + "\f_preview_url\"*\n" + + "\x14PortUnexposedPayload\x12\x12\n" + + "\x04port\x18\x01 \x01(\x05R\x04port\"s\n" + "\x10RepoClonePayload\x12\x12\n" + "\x04repo\x18\x01 \x01(\tR\x04repo\x121\n" + "\x05stage\x18\x02 \x01(\x0e2\x1b.netclode.v1.RepoCloneStageR\x05stage\x12\x18\n" + - "\amessage\x18\x03 \x01(\tR\amessage*\x87\x03\n" + + "\amessage\x18\x03 \x01(\tR\amessage*\xac\x03\n" + "\x0eAgentEventKind\x12 \n" + "\x1cAGENT_EVENT_KIND_UNSPECIFIED\x10\x00\x12\x1c\n" + "\x18AGENT_EVENT_KIND_MESSAGE\x10\x01\x12\x1d\n" + @@ -938,7 +1005,8 @@ const file_netclode_v1_events_proto_rawDesc = "" + "\x1bAGENT_EVENT_KIND_REPO_CLONE\x10\b\x12'\n" + "#AGENT_EVENT_KIND_AGENT_DISCONNECTED\x10\t\x12&\n" + "\"AGENT_EVENT_KIND_AGENT_RECONNECTED\x10\n" + - "*^\n" + + "\x12#\n" + + "\x1fAGENT_EVENT_KIND_PORT_UNEXPOSED\x10\v*^\n" + "\vMessageRole\x12\x1c\n" + "\x18MESSAGE_ROLE_UNSPECIFIED\x10\x00\x12\x15\n" + "\x11MESSAGE_ROLE_USER\x10\x01\x12\x1a\n" + @@ -964,21 +1032,22 @@ func file_netclode_v1_events_proto_rawDescGZIP() []byte { } var file_netclode_v1_events_proto_enumTypes = make([]protoimpl.EnumInfo, 3) -var file_netclode_v1_events_proto_msgTypes = make([]protoimpl.MessageInfo, 9) +var file_netclode_v1_events_proto_msgTypes = make([]protoimpl.MessageInfo, 10) var file_netclode_v1_events_proto_goTypes = []any{ - (AgentEventKind)(0), // 0: netclode.v1.AgentEventKind - (MessageRole)(0), // 1: netclode.v1.MessageRole - (RepoCloneStage)(0), // 2: netclode.v1.RepoCloneStage - (*AgentEvent)(nil), // 3: netclode.v1.AgentEvent - (*MessagePayload)(nil), // 4: netclode.v1.MessagePayload - (*ThinkingPayload)(nil), // 5: netclode.v1.ThinkingPayload - (*ToolStartPayload)(nil), // 6: netclode.v1.ToolStartPayload - (*ToolInputPayload)(nil), // 7: netclode.v1.ToolInputPayload - (*ToolOutputPayload)(nil), // 8: netclode.v1.ToolOutputPayload - (*ToolEndPayload)(nil), // 9: netclode.v1.ToolEndPayload - (*PortExposedPayload)(nil), // 10: netclode.v1.PortExposedPayload - (*RepoClonePayload)(nil), // 11: netclode.v1.RepoClonePayload - (*structpb.Struct)(nil), // 12: google.protobuf.Struct + (AgentEventKind)(0), // 0: netclode.v1.AgentEventKind + (MessageRole)(0), // 1: netclode.v1.MessageRole + (RepoCloneStage)(0), // 2: netclode.v1.RepoCloneStage + (*AgentEvent)(nil), // 3: netclode.v1.AgentEvent + (*MessagePayload)(nil), // 4: netclode.v1.MessagePayload + (*ThinkingPayload)(nil), // 5: netclode.v1.ThinkingPayload + (*ToolStartPayload)(nil), // 6: netclode.v1.ToolStartPayload + (*ToolInputPayload)(nil), // 7: netclode.v1.ToolInputPayload + (*ToolOutputPayload)(nil), // 8: netclode.v1.ToolOutputPayload + (*ToolEndPayload)(nil), // 9: netclode.v1.ToolEndPayload + (*PortExposedPayload)(nil), // 10: netclode.v1.PortExposedPayload + (*PortUnexposedPayload)(nil), // 11: netclode.v1.PortUnexposedPayload + (*RepoClonePayload)(nil), // 12: netclode.v1.RepoClonePayload + (*structpb.Struct)(nil), // 13: google.protobuf.Struct } var file_netclode_v1_events_proto_depIdxs = []int32{ 0, // 0: netclode.v1.AgentEvent.kind:type_name -> netclode.v1.AgentEventKind @@ -989,15 +1058,16 @@ var file_netclode_v1_events_proto_depIdxs = []int32{ 8, // 5: netclode.v1.AgentEvent.tool_output:type_name -> netclode.v1.ToolOutputPayload 9, // 6: netclode.v1.AgentEvent.tool_end:type_name -> netclode.v1.ToolEndPayload 10, // 7: netclode.v1.AgentEvent.port_exposed:type_name -> netclode.v1.PortExposedPayload - 11, // 8: netclode.v1.AgentEvent.repo_clone:type_name -> netclode.v1.RepoClonePayload - 1, // 9: netclode.v1.MessagePayload.role:type_name -> netclode.v1.MessageRole - 12, // 10: netclode.v1.ToolInputPayload.input:type_name -> google.protobuf.Struct - 2, // 11: netclode.v1.RepoClonePayload.stage:type_name -> netclode.v1.RepoCloneStage - 12, // [12:12] is the sub-list for method output_type - 12, // [12:12] is the sub-list for method input_type - 12, // [12:12] is the sub-list for extension type_name - 12, // [12:12] is the sub-list for extension extendee - 0, // [0:12] is the sub-list for field type_name + 12, // 8: netclode.v1.AgentEvent.repo_clone:type_name -> netclode.v1.RepoClonePayload + 11, // 9: netclode.v1.AgentEvent.port_unexposed:type_name -> netclode.v1.PortUnexposedPayload + 1, // 10: netclode.v1.MessagePayload.role:type_name -> netclode.v1.MessageRole + 13, // 11: netclode.v1.ToolInputPayload.input:type_name -> google.protobuf.Struct + 2, // 12: netclode.v1.RepoClonePayload.stage:type_name -> netclode.v1.RepoCloneStage + 13, // [13:13] is the sub-list for method output_type + 13, // [13:13] is the sub-list for method input_type + 13, // [13:13] is the sub-list for extension type_name + 13, // [13:13] is the sub-list for extension extendee + 0, // [0:13] is the sub-list for field type_name } func init() { file_netclode_v1_events_proto_init() } @@ -1014,6 +1084,7 @@ func file_netclode_v1_events_proto_init() { (*AgentEvent_ToolEnd)(nil), (*AgentEvent_PortExposed)(nil), (*AgentEvent_RepoClone)(nil), + (*AgentEvent_PortUnexposed)(nil), } file_netclode_v1_events_proto_msgTypes[3].OneofWrappers = []any{} file_netclode_v1_events_proto_msgTypes[4].OneofWrappers = []any{} @@ -1026,7 +1097,7 @@ func file_netclode_v1_events_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_netclode_v1_events_proto_rawDesc), len(file_netclode_v1_events_proto_rawDesc)), NumEnums: 3, - NumMessages: 9, + NumMessages: 10, NumExtensions: 0, NumServices: 0, }, diff --git a/services/control-plane/internal/api/connect_agent.go b/services/control-plane/internal/api/connect_agent.go index 3a1e2652..f3e4450f 100644 --- a/services/control-plane/internal/api/connect_agent.go +++ b/services/control-plane/internal/api/connect_agent.go @@ -13,6 +13,7 @@ import ( v1 "github.com/angristan/netclode/services/control-plane/gen/netclode/v1" "github.com/angristan/netclode/services/control-plane/gen/netclode/v1/netclodev1connect" "github.com/angristan/netclode/services/control-plane/internal/session" + "google.golang.org/protobuf/types/known/timestamppb" ) // Ensure ConnectAgentServiceHandler implements the interface @@ -199,6 +200,9 @@ func (h *ConnectAgentServiceHandler) Connect(ctx context.Context, stream *connec if config.GitHubToken != "" { sessionConfig.GithubToken = &config.GitHubToken } + if config.GitHubCopilotToken != "" { + sessionConfig.GithubCopilotToken = &config.GitHubCopilotToken + } if config.Model != "" { sessionConfig.Model = &config.Model } @@ -208,8 +212,11 @@ func (h *ConnectAgentServiceHandler) Connect(ctx context.Context, stream *connec if config.CodexIdToken != "" { sessionConfig.CodexIdToken = &config.CodexIdToken } - if config.CodexRefreshToken != "" { - sessionConfig.CodexRefreshToken = &config.CodexRefreshToken + if config.OpenAIAPIKey != "" { + sessionConfig.OpenaiApiKey = &config.OpenAIAPIKey + } + if config.MistralAPIKey != "" { + sessionConfig.MistralApiKey = &config.MistralAPIKey } if config.ReasoningEffort != "" { sessionConfig.ReasoningEffort = &config.ReasoningEffort @@ -217,6 +224,12 @@ func (h *ConnectAgentServiceHandler) Connect(ctx context.Context, stream *connec if config.OllamaURL != "" { sessionConfig.OllamaUrl = &config.OllamaURL } + if config.OpenCodeAPIKey != "" { + sessionConfig.OpencodeApiKey = &config.OpenCodeAPIKey + } + if config.ZaiAPIKey != "" { + sessionConfig.ZaiApiKey = &config.ZaiAPIKey + } if err := conn.send(&v1.ControlPlaneMessage{ Message: &v1.ControlPlaneMessage_Registered{ @@ -447,6 +460,22 @@ func (c *AgentConnection) UpdateGitCredentials(token string, repoAccess v1.RepoA }) } +// UpdateCodexAuth sends updated short-lived Codex OAuth tokens to the agent. +func (c *AgentConnection) UpdateCodexAuth(accessToken, idToken string, expiresAt *timestamppb.Timestamp) error { + update := &v1.UpdateCodexAuth{ + AccessToken: accessToken, + IdToken: idToken, + } + if expiresAt != nil { + update.ExpiresAt = expiresAt + } + return c.Send(&v1.ControlPlaneMessage{ + Message: &v1.ControlPlaneMessage_UpdateCodexAuth{ + UpdateCodexAuth: update, + }, + }) +} + // AssignSession assigns a session to a warm pool agent (implements WarmAgentConnection). // This pushes the SessionAssigned message to the agent for instant session start. func (c *AgentConnection) AssignSession(sessionID string, config *session.AgentSessionConfig) error { @@ -469,6 +498,9 @@ func (c *AgentConnection) AssignSession(sessionID string, config *session.AgentS if config.GitHubToken != "" { sessionConfig.GithubToken = &config.GitHubToken } + if config.GitHubCopilotToken != "" { + sessionConfig.GithubCopilotToken = &config.GitHubCopilotToken + } if config.Model != "" { sessionConfig.Model = &config.Model } @@ -478,8 +510,11 @@ func (c *AgentConnection) AssignSession(sessionID string, config *session.AgentS if config.CodexIdToken != "" { sessionConfig.CodexIdToken = &config.CodexIdToken } - if config.CodexRefreshToken != "" { - sessionConfig.CodexRefreshToken = &config.CodexRefreshToken + if config.OpenAIAPIKey != "" { + sessionConfig.OpenaiApiKey = &config.OpenAIAPIKey + } + if config.MistralAPIKey != "" { + sessionConfig.MistralApiKey = &config.MistralAPIKey } if config.ReasoningEffort != "" { sessionConfig.ReasoningEffort = &config.ReasoningEffort @@ -487,6 +522,12 @@ func (c *AgentConnection) AssignSession(sessionID string, config *session.AgentS if config.OllamaURL != "" { sessionConfig.OllamaUrl = &config.OllamaURL } + if config.OpenCodeAPIKey != "" { + sessionConfig.OpencodeApiKey = &config.OpenCodeAPIKey + } + if config.ZaiAPIKey != "" { + sessionConfig.ZaiApiKey = &config.ZaiAPIKey + } slog.Info("Pushing session assignment to warm agent", "sessionID", sessionID, "podName", c.podName) diff --git a/services/control-plane/internal/api/connect_client.go b/services/control-plane/internal/api/connect_client.go index 2e928383..d2b04381 100644 --- a/services/control-plane/internal/api/connect_client.go +++ b/services/control-plane/internal/api/connect_client.go @@ -193,6 +193,8 @@ func (c *ConnectConnection) handleMessage(ctx context.Context, msg *pb.ClientMes return c.handleSync(ctx) case *pb.ClientMessage_ExposePort: return c.handlePortExpose(ctx, m.ExposePort.SessionId, int(m.ExposePort.Port)) + case *pb.ClientMessage_UnexposePort: + return c.handlePortUnexpose(ctx, m.UnexposePort.SessionId, int(m.UnexposePort.Port)) case *pb.ClientMessage_ListGithubRepos: return c.handleGitHubReposList(ctx) case *pb.ClientMessage_GitStatus: @@ -211,6 +213,12 @@ func (c *ConnectConnection) handleMessage(ctx context.Context, msg *pb.ClientMes return c.handleUpdateRepoAccess(ctx, m.UpdateRepoAccess) case *pb.ClientMessage_GetResourceLimits: return c.handleGetResourceLimits(ctx, m.GetResourceLimits) + case *pb.ClientMessage_CodexAuthStart: + return c.handleCodexAuthStart(ctx, m.CodexAuthStart) + case *pb.ClientMessage_CodexAuthStatus: + return c.handleCodexAuthStatus(ctx, m.CodexAuthStatus) + case *pb.ClientMessage_CodexAuthLogout: + return c.handleCodexAuthLogout(ctx, m.CodexAuthLogout) default: return connect.NewError(connect.CodeInvalidArgument, errUnknownMessage) } @@ -371,11 +379,19 @@ func (c *ConnectConnection) handleSessionCreate(ctx context.Context, req *pb.Cre if req.Resources != nil { resourcesPtr = req.Resources } - name := "" if req.Name != nil { name = *req.Name } + if req.CodexOauthTokens != nil { + return connect.NewError(connect.CodeInvalidArgument, errors.New("codex_oauth_tokens are no longer accepted; use backend codex auth flow")) + } + + if sdkTypePtr != nil && *sdkTypePtr == pb.SdkType_SDK_TYPE_CODEX { + if modelPtr == nil || codexModelAuthMode(*modelPtr) == "" { + return connect.NewError(connect.CodeInvalidArgument, errors.New("codex model must include auth suffix (:oauth or :api), e.g. gpt-5.2-codex:oauth:high")) + } + } sess, err := c.manager.Create(ctx, name, repos, repoAccessPtr, sdkTypePtr, modelPtr, copilotBackendPtr, tailnetAccessPtr, resourcesPtr) if err != nil { @@ -640,6 +656,28 @@ func (c *ConnectConnection) handlePortExpose(ctx context.Context, sessionID stri }) } +func (c *ConnectConnection) handlePortUnexpose(ctx context.Context, sessionID string, port int) error { + if sessionID == "" { + return c.send(makeErrorResponse(sessionID, "PORT_ERROR", "sessionId is required")) + } + if port < 1 || port > 65535 { + return c.send(makeErrorResponse(sessionID, "PORT_ERROR", "port must be between 1 and 65535")) + } + + if err := c.manager.UnexposePort(ctx, sessionID, port); err != nil { + return c.send(makeErrorResponse(sessionID, "PORT_ERROR", err.Error())) + } + + return c.send(&pb.ServerMessage{ + Message: &pb.ServerMessage_PortUnexposed{ + PortUnexposed: &pb.PortUnexposedResponse{ + SessionId: sessionID, + Port: int32(port), + }, + }, + }) +} + func (c *ConnectConnection) handleGitHubReposList(ctx context.Context) error { repos, err := c.manager.ListGitHubRepos(ctx) if err != nil { @@ -731,6 +769,25 @@ func (c *ConnectConnection) handleListModels(ctx context.Context, req *pb.ListMo }) } +func codexModelAuthMode(model string) string { + parts := strings.Split(model, ":") + if len(parts) < 2 { + return "" + } + last := parts[len(parts)-1] + if last == "minimal" || last == "low" || last == "medium" || last == "high" || last == "xhigh" { + if len(parts) >= 3 { + last = parts[len(parts)-2] + } else { + return "" + } + } + if last == "api" || last == "oauth" { + return last + } + return "" +} + // handleGetCopilotStatus returns GitHub Copilot authentication status and quota. func (c *ConnectConnection) handleGetCopilotStatus(ctx context.Context) error { status := c.manager.GetCopilotStatus(ctx) @@ -823,6 +880,45 @@ func (c *ConnectConnection) handleGetResourceLimits(ctx context.Context, req *pb }) } +func (c *ConnectConnection) handleCodexAuthStart(ctx context.Context, req *pb.CodexAuthStartRequest) error { + resp, err := c.manager.StartCodexAuth(ctx) + if err != nil { + return c.send(makeErrorResponse("", "CODEX_AUTH_ERROR", err.Error())) + } + resp.RequestId = req.RequestId + return c.send(&pb.ServerMessage{ + Message: &pb.ServerMessage_CodexAuthStarted{ + CodexAuthStarted: resp, + }, + }) +} + +func (c *ConnectConnection) handleCodexAuthStatus(ctx context.Context, req *pb.CodexAuthStatusRequest) error { + resp, err := c.manager.GetCodexAuthStatus(ctx) + if err != nil { + return c.send(makeErrorResponse("", "CODEX_AUTH_ERROR", err.Error())) + } + resp.RequestId = req.RequestId + return c.send(&pb.ServerMessage{ + Message: &pb.ServerMessage_CodexAuthStatus{ + CodexAuthStatus: resp, + }, + }) +} + +func (c *ConnectConnection) handleCodexAuthLogout(ctx context.Context, req *pb.CodexAuthLogoutRequest) error { + if err := c.manager.LogoutCodexAuth(ctx); err != nil { + return c.send(makeErrorResponse("", "CODEX_AUTH_ERROR", err.Error())) + } + return c.send(&pb.ServerMessage{ + Message: &pb.ServerMessage_CodexAuthLoggedOut{ + CodexAuthLoggedOut: &pb.CodexAuthLoggedOutResponse{ + RequestId: req.RequestId, + }, + }, + }) +} + // storageEntryToProto converts a storage.StreamEntryWithID to a pb.StreamEntry. // The new unified model uses oneof payload with: AgentEvent, TerminalOutput, Session, Error func storageEntryToProto(e storage.StreamEntryWithID) *pb.StreamEntry { diff --git a/services/control-plane/internal/config/config.go b/services/control-plane/internal/config/config.go index 84c265b5..1a2279f1 100644 --- a/services/control-plane/internal/config/config.go +++ b/services/control-plane/internal/config/config.go @@ -30,10 +30,8 @@ type Config struct { CPUOvercommitRatio int MemoryOvercommitRatio int - // Codex OAuth tokens (for ChatGPT auth mode) - CodexAccessToken string - CodexIdToken string - CodexRefreshToken string + // Session-scoped Codex OAuth vault encryption key (base64-encoded 32-byte key in env) + CodexOAuthEncryptionKey []byte // GitHub App integration (for repo-scoped tokens) GitHubAppID int64 @@ -70,10 +68,7 @@ func Load() *Config { CPUOvercommitRatio: getEnvInt("CPU_OVERCOMMIT_RATIO", 1), // 1 = no overcommit MemoryOvercommitRatio: getEnvInt("MEMORY_OVERCOMMIT_RATIO", 1), // 1 = no overcommit - // Codex OAuth tokens - CodexAccessToken: getEnv("CODEX_ACCESS_TOKEN", ""), - CodexIdToken: getEnv("CODEX_ID_TOKEN", ""), - CodexRefreshToken: getEnv("CODEX_REFRESH_TOKEN", ""), + CodexOAuthEncryptionKey: getCodexOAuthEncryptionKey(), // GitHub App integration GitHubAppID: getEnvInt64("GITHUB_APP_ID", 0), @@ -106,6 +101,18 @@ func getGitHubPrivateKey() string { return os.Getenv("GITHUB_APP_PRIVATE_KEY") } +func getCodexOAuthEncryptionKey() []byte { + keyB64 := os.Getenv("CODEX_OAUTH_ENCRYPTION_KEY_B64") + if keyB64 == "" { + return nil + } + decoded, err := base64.StdEncoding.DecodeString(keyB64) + if err != nil || len(decoded) != 32 { + return nil + } + return decoded +} + // HasGitHubApp returns true if GitHub App is configured. func (c *Config) HasGitHubApp() bool { return c.GitHubAppID > 0 && c.GitHubAppPrivateKey != "" && c.GitHubInstallationID > 0 diff --git a/services/control-plane/internal/k8s/client.go b/services/control-plane/internal/k8s/client.go index 466a59ec..7fd2c655 100644 --- a/services/control-plane/internal/k8s/client.go +++ b/services/control-plane/internal/k8s/client.go @@ -42,6 +42,8 @@ type Runtime interface { DeleteSandboxService(ctx context.Context, sessionID string) error ListTailscaleServices(ctx context.Context) ([]string, error) // Returns session IDs with ts-* services ExposePort(ctx context.Context, sessionID string, port int) error + UnexposePort(ctx context.Context, sessionID string, port int) error + GetSandboxPreviewHostname(ctx context.Context, sessionID string) (string, error) // Network policy operations ConfigureNetwork(ctx context.Context, sessionID string, networkEnabled bool) error @@ -57,6 +59,7 @@ type Runtime interface { CreatePVCFromSnapshot(ctx context.Context, sessionID, snapshotID string) (pvcName string, err error) WaitForRestoreJob(ctx context.Context, sessionID, snapshotID string, timeout time.Duration) error GetPVCName(ctx context.Context, sessionID string) (string, error) + PVCExists(ctx context.Context, pvcName string) (bool, error) // Agent authentication // VerifyAgentToken validates a Kubernetes ServiceAccount token and returns the pod name. diff --git a/services/control-plane/internal/k8s/sandbox.go b/services/control-plane/internal/k8s/sandbox.go index a8834b1d..f31a6767 100644 --- a/services/control-plane/internal/k8s/sandbox.go +++ b/services/control-plane/internal/k8s/sandbox.go @@ -11,6 +11,7 @@ import ( "github.com/angristan/netclode/services/control-plane/internal/config" authenticationv1 "k8s.io/api/authentication/v1" + batchv1 "k8s.io/api/batch/v1" corev1 "k8s.io/api/core/v1" networkingv1 "k8s.io/api/networking/v1" "k8s.io/apimachinery/pkg/api/errors" @@ -56,6 +57,8 @@ type k8sRuntime struct { claimCache map[string]*SandboxClaim } +const controlPlaneIngressName = "control-plane" + func newK8sRuntime(cfg *config.Config) (*k8sRuntime, error) { restConfig, err := rest.InClusterConfig() if err != nil { @@ -511,13 +514,14 @@ func (r *k8sRuntime) buildContainerResources(resources *SandboxResourceConfig) * // WaitForReady registers a callback to be called when sandbox becomes ready. // Uses informer-based watching instead of polling. func (r *k8sRuntime) WaitForReady(ctx context.Context, sessionID string, timeout time.Duration) (string, error) { - // Check if already ready from cache - r.cacheMu.RLock() - sandbox, exists := r.sandboxCache[sessionID] - r.cacheMu.RUnlock() - - if exists && sandbox.IsReady() { - return r.getServiceFQDN(sandbox), nil + // Check the latest sandbox object from the API first. + // Relying only on informer cache can return stale readiness after rapid delete/recreate cycles. + if status, err := r.GetStatus(ctx, sessionID); err == nil { + if status.Exists && status.Ready { + return status.ServiceFQDN, nil + } + } else { + slog.Warn("Failed to get fresh sandbox status, falling back to informer wait", "sessionID", sessionID, "error", err) } // Setup callback channel @@ -580,28 +584,51 @@ func (r *k8sRuntime) WatchSandboxReady(sessionID string, callback SandboxReadyCa r.callbacksMu.Unlock() } -// GetStatus retrieves the status of a sandbox from cache. +// GetStatus retrieves the status of a sandbox from the API server. func (r *k8sRuntime) GetStatus(ctx context.Context, sessionID string) (*SandboxStatusInfo, error) { - r.cacheMu.RLock() - sandbox, exists := r.sandboxCache[sessionID] - r.cacheMu.RUnlock() - - if !exists { - // Try fetching directly - name := sandboxName(sessionID) - u, err := r.dynamicClient.Resource(SandboxGVR).Namespace(r.namespace).Get(ctx, name, metav1.GetOptions{}) - if err != nil { - if errors.IsNotFound(err) { - return &SandboxStatusInfo{Exists: false}, nil - } - return nil, err + list, err := r.dynamicClient.Resource(SandboxGVR).Namespace(r.namespace).List(ctx, metav1.ListOptions{ + LabelSelector: fmt.Sprintf("netclode.io/session=%s", sessionID), + }) + if err != nil { + // Fallback to cache only if the API request itself fails. + r.cacheMu.RLock() + sandbox, exists := r.sandboxCache[sessionID] + r.cacheMu.RUnlock() + if exists { + return &SandboxStatusInfo{ + Exists: true, + Ready: sandbox.IsReady(), + ServiceFQDN: r.getServiceFQDN(sandbox), + Error: sandbox.GetError(), + }, nil } - sandbox = r.unstructuredToSandbox(u) - if sandbox == nil { - return &SandboxStatusInfo{Exists: false}, nil + return nil, err + } + + if len(list.Items) == 0 { + // API confirms no sandbox with this session label exists. + return &SandboxStatusInfo{Exists: false}, nil + } + + // If multiple objects exist transiently, use the newest one. + latest := &list.Items[0] + for i := 1; i < len(list.Items); i++ { + item := &list.Items[i] + if item.GetCreationTimestamp().After(latest.GetCreationTimestamp().Time) { + latest = item } } + sandbox := r.unstructuredToSandbox(latest) + if sandbox == nil { + return &SandboxStatusInfo{Exists: false}, nil + } + + // Refresh cache with the latest object. + r.cacheMu.Lock() + r.sandboxCache[sessionID] = sandbox + r.cacheMu.Unlock() + return &SandboxStatusInfo{ Exists: true, Ready: sandbox.IsReady(), @@ -656,6 +683,18 @@ func (r *k8sRuntime) DeletePVC(ctx context.Context, sessionID string) error { return nil } +// PVCExists checks whether a PVC with the given name exists. +func (r *k8sRuntime) PVCExists(ctx context.Context, name string) (bool, error) { + _, err := r.clientset.CoreV1().PersistentVolumeClaims(r.namespace).Get(ctx, name, metav1.GetOptions{}) + if err != nil { + if errors.IsNotFound(err) { + return false, nil + } + return false, err + } + return true, nil +} + // DeletePVCByName deletes a PVC by its exact name. func (r *k8sRuntime) DeletePVCByName(ctx context.Context, name string) error { err := r.clientset.CoreV1().PersistentVolumeClaims(r.namespace).Delete(ctx, name, metav1.DeleteOptions{}) @@ -973,6 +1012,152 @@ func (r *k8sRuntime) ExposePort(ctx context.Context, sessionID string, port int) return nil } +// UnexposePort removes a previously exposed port from the Tailscale service and NetworkPolicy. +// This is called when a user removes a preview port. +func (r *k8sRuntime) UnexposePort(ctx context.Context, sessionID string, port int) error { + tailscaleSvcName := fmt.Sprintf("ts-%s", sessionID) + networkPolicyName := fmt.Sprintf("sess-%s-network-policy", sessionID) + removedServicePort := false + + // 1. Remove port from the Tailscale service + svc, err := r.clientset.CoreV1().Services(r.namespace).Get(ctx, tailscaleSvcName, metav1.GetOptions{}) + if err != nil { + if errors.IsNotFound(err) { + slog.Info("Tailscale service not found during unexpose, skipping service update", "sessionID", sessionID, "name", tailscaleSvcName, "port", port) + } else { + return fmt.Errorf("get tailscale service: %w", err) + } + } else { + servicePorts := make([]corev1.ServicePort, 0, len(svc.Spec.Ports)) + for _, p := range svc.Spec.Ports { + if p.Port == int32(port) { + removedServicePort = true + continue + } + servicePorts = append(servicePorts, p) + } + + if removedServicePort { + svc.Spec.Ports = servicePorts + if _, err := r.clientset.CoreV1().Services(r.namespace).Update(ctx, svc, metav1.UpdateOptions{}); err != nil { + return fmt.Errorf("update service: %w", err) + } + slog.Info("Removed port from Tailscale service", "sessionID", sessionID, "port", port) + } + } + + // 2. Remove port from the NetworkPolicy + np, err := r.clientset.NetworkingV1().NetworkPolicies(r.namespace).Get(ctx, networkPolicyName, metav1.GetOptions{}) + if err != nil { + // NetworkPolicy might not exist (e.g., if sandbox was created without one) + if errors.IsNotFound(err) { + slog.Warn("NetworkPolicy not found, skipping", "sessionID", sessionID, "name", networkPolicyName) + return nil + } + return fmt.Errorf("get network policy: %w", err) + } + + removedPolicyPort := false + updatedIngress := make([]networkingv1.NetworkPolicyIngressRule, 0, len(np.Spec.Ingress)) + for _, rule := range np.Spec.Ingress { + isTailscaleRule := false + for _, from := range rule.From { + if from.NamespaceSelector != nil && + from.NamespaceSelector.MatchLabels["kubernetes.io/metadata.name"] == "tailscale" { + isTailscaleRule = true + break + } + } + + if !isTailscaleRule { + updatedIngress = append(updatedIngress, rule) + continue + } + + filteredPorts := make([]networkingv1.NetworkPolicyPort, 0, len(rule.Ports)) + removedFromThisRule := false + for _, p := range rule.Ports { + if p.Port != nil && p.Port.IntValue() == port { + removedFromThisRule = true + continue + } + filteredPorts = append(filteredPorts, p) + } + + if !removedFromThisRule { + updatedIngress = append(updatedIngress, rule) + continue + } + + removedPolicyPort = true + if len(filteredPorts) == 0 { + // Rule only allowed this port; remove the whole rule. + continue + } + + rule.Ports = filteredPorts + updatedIngress = append(updatedIngress, rule) + } + + if removedPolicyPort { + np.Spec.Ingress = updatedIngress + if _, err := r.clientset.NetworkingV1().NetworkPolicies(r.namespace).Update(ctx, np, metav1.UpdateOptions{}); err != nil { + return fmt.Errorf("update network policy: %w", err) + } + slog.Info("Removed port from NetworkPolicy", "sessionID", sessionID, "port", port) + } + + if !removedServicePort && !removedPolicyPort { + slog.Info("Port was not exposed; no unexpose changes applied", "sessionID", sessionID, "port", port) + } + + return nil +} + +// GetSandboxPreviewHostname returns the best available hostname for sandbox previews. +// It prefers the explicit Tailscale service hostname, and if that hostname is short +// (for example `sandbox-abc123`), it appends the tailnet DNS suffix inferred from +// the control-plane ingress hostname. +func (r *k8sRuntime) GetSandboxPreviewHostname(ctx context.Context, sessionID string) (string, error) { + fallbackHost := fmt.Sprintf("sandbox-%s", sessionID) + tailscaleSvcName := fmt.Sprintf("ts-%s", sessionID) + + svc, err := r.clientset.CoreV1().Services(r.namespace).Get(ctx, tailscaleSvcName, metav1.GetOptions{}) + if err != nil { + return fallbackHost, fmt.Errorf("get tailscale service: %w", err) + } + + host := strings.TrimSpace(svc.Annotations["tailscale.com/hostname"]) + if host == "" { + host = fallbackHost + } + + // If already fully-qualified, use as-is. + if strings.Contains(host, ".") { + return host, nil + } + + ingress, err := r.clientset.NetworkingV1().Ingresses(r.namespace).Get(ctx, controlPlaneIngressName, metav1.GetOptions{}) + if err != nil { + return host, fmt.Errorf("get control-plane ingress: %w", err) + } + if len(ingress.Status.LoadBalancer.Ingress) == 0 { + return host, fmt.Errorf("control-plane ingress has no load balancer hostname") + } + + ingressHost := strings.TrimSpace(ingress.Status.LoadBalancer.Ingress[0].Hostname) + if ingressHost == "" { + return host, fmt.Errorf("control-plane ingress load balancer hostname is empty") + } + + _, tailnetSuffix, ok := strings.Cut(ingressHost, ".") + if !ok || tailnetSuffix == "" { + return host, fmt.Errorf("cannot infer tailnet suffix from ingress hostname %q", ingressHost) + } + + return fmt.Sprintf("%s.%s", host, tailnetSuffix), nil +} + // ConfigureNetwork enables or disables internet access for a sandbox. // ConfigureNetwork adds or removes internet access for a sandbox. // The default SandboxTemplate has NO internet access (only DNS + control-plane). @@ -1797,9 +1982,10 @@ func strPtr(s string) *string { return &s } -// WaitForRestoreJob waits for the JuiceFS restore job to complete. -// JuiceFS CSI creates a restore job when a PVC is created from a snapshot. -// The job name follows the pattern: juicefs-restore-snapshot-{volumesnapshotcontent-uid} +// WaitForRestoreJob waits for the JuiceFS restore job to complete if one is created. +// Some JuiceFS deployments restore snapshot data without a Job object, so this method +// performs bounded discovery and returns success when no async restore job appears. +// Expected job name pattern: juicefs-restore-snapshot-{volumesnapshotcontent-uid}. func (r *k8sRuntime) WaitForRestoreJob(ctx context.Context, sessionID, snapshotID string, timeout time.Duration) error { snapName := snapshotName(sessionID, snapshotID) @@ -1834,23 +2020,83 @@ func (r *k8sRuntime) WaitForRestoreJob(ctx context.Context, sessionID, snapshotI // JuiceFS restore job name follows pattern: juicefs-restore-snapshot-{uid} jobName := fmt.Sprintf("juicefs-restore-snapshot-%s", uid) + const ( + pollInterval = 500 * time.Millisecond + discoveryTimeout = 10 * time.Second + ) + slog.Info("Waiting for JuiceFS restore job", "sessionID", sessionID, "snapshotID", snapshotID, "jobName", jobName) deadline := time.Now().Add(timeout) + discoveryDeadline := time.Now().Add(discoveryTimeout) + jobSeen := false + lastNamespace := "" + + sleepOrCancel := func() error { + select { + case <-ctx.Done(): + return ctx.Err() + case <-time.After(pollInterval): + return nil + } + } + for time.Now().Before(deadline) { - job, err := r.clientset.BatchV1().Jobs("kube-system").Get(ctx, jobName, metav1.GetOptions{}) - if err != nil { - if errors.IsNotFound(err) { - // Job might not exist yet, wait and retry - time.Sleep(500 * time.Millisecond) - continue + select { + case <-ctx.Done(): + return ctx.Err() + default: + } + + var ( + job *batchv1.Job + ns string + found bool + ) + + // Historically this job lived in kube-system; also check current control-plane namespace. + // This avoids hard-coding a single namespace across cluster setups. + candidateNamespaces := []string{"kube-system"} + if r.namespace != "kube-system" { + candidateNamespaces = append(candidateNamespaces, r.namespace) + } + + for _, candidateNS := range candidateNamespaces { + j, err := r.clientset.BatchV1().Jobs(candidateNS).Get(ctx, jobName, metav1.GetOptions{}) + if err != nil { + if errors.IsNotFound(err) { + continue + } + return fmt.Errorf("get restore job %s in namespace %s: %w", jobName, candidateNS, err) } - return fmt.Errorf("get restore job %s: %w", jobName, err) + job = j + ns = candidateNS + found = true + break } + if !found { + if jobSeen { + // Job existed earlier and is now gone; with job TTL cleanup this usually means completion. + slog.Info("Restore job disappeared after being observed; assuming completion", "sessionID", sessionID, "jobName", jobName, "namespace", lastNamespace) + return nil + } + if time.Now().After(discoveryDeadline) { + slog.Info("No JuiceFS restore job detected; continuing without async restore wait", "sessionID", sessionID, "snapshotID", snapshotID, "jobName", jobName) + return nil + } + if err := sleepOrCancel(); err != nil { + return err + } + continue + } + + jobSeen = true + lastNamespace = ns + // Check job status if job.Status.Succeeded > 0 { - slog.Info("JuiceFS restore job completed successfully", "sessionID", sessionID, "jobName", jobName) + slog.Info("JuiceFS restore job completed successfully", "sessionID", sessionID, "jobName", jobName, "namespace", ns) return nil } @@ -1858,10 +2104,16 @@ func (r *k8sRuntime) WaitForRestoreJob(ctx context.Context, sessionID, snapshotI return fmt.Errorf("restore job %s failed after %d attempts", jobName, job.Status.Failed) } - slog.Debug("Restore job still running", "sessionID", sessionID, "jobName", jobName, "active", job.Status.Active, "succeeded", job.Status.Succeeded, "failed", job.Status.Failed) - time.Sleep(500 * time.Millisecond) + slog.Debug("Restore job still running", "sessionID", sessionID, "jobName", jobName, "namespace", ns, "active", job.Status.Active, "succeeded", job.Status.Succeeded, "failed", job.Status.Failed) + if err := sleepOrCancel(); err != nil { + return err + } } + if !jobSeen { + slog.Warn("Timed out waiting for restore job discovery; continuing", "sessionID", sessionID, "snapshotID", snapshotID, "jobName", jobName) + return nil + } return fmt.Errorf("timeout waiting for restore job %s", jobName) } diff --git a/services/control-plane/internal/session/agent.go b/services/control-plane/internal/session/agent.go index a2692286..9b856693 100644 --- a/services/control-plane/internal/session/agent.go +++ b/services/control-plane/internal/session/agent.go @@ -9,6 +9,7 @@ import ( pb "github.com/angristan/netclode/services/control-plane/gen/netclode/v1" "github.com/google/uuid" + "google.golang.org/protobuf/types/known/timestamppb" ) const ( @@ -52,6 +53,25 @@ func (m *Manager) SendPrompt(ctx context.Context, sessionID, text string) error return nil } + // Ensure Codex OAuth access/id tokens are fresh before prompt execution. + // Refresh token is stored server-side only and never sent to sandbox. + if state.Session.SdkType != nil && *state.Session.SdkType == pb.SdkType_SDK_TYPE_CODEX && state.Session.Model != nil { + authMode, _ := parseCodexAuthModeAndEffort(*state.Session.Model) + if authMode == "oauth" { + oauthData, err := m.prepareCodexOAuthForPrompt(ctx, sessionID) + if err != nil { + return fmt.Errorf("prepare codex oauth: %w", err) + } + var expiresAt *timestamppb.Timestamp + if oauthData.ExpiresAt != nil { + expiresAt = timestamppb.New(*oauthData.ExpiresAt) + } + if err := agent.UpdateCodexAuth(oauthData.AccessToken, oauthData.IdToken, expiresAt); err != nil { + return fmt.Errorf("send codex oauth update to agent: %w", err) + } + } + } + // Persist and broadcast user message (emitUserMessage does both via publishStreamEntry) m.emitUserMessage(ctx, sessionID, text) diff --git a/services/control-plane/internal/session/agent_handlers.go b/services/control-plane/internal/session/agent_handlers.go index 060f3c3c..4fde6aa6 100644 --- a/services/control-plane/internal/session/agent_handlers.go +++ b/services/control-plane/internal/session/agent_handlers.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "log/slog" + "strings" "time" pb "github.com/angristan/netclode/services/control-plane/gen/netclode/v1" @@ -27,8 +28,7 @@ func (m *Manager) HandleAgentResponse(ctx context.Context, sessionID string, res case *pb.AgentStreamResponse_Event: return m.handleAgentEvent(ctx, sessionID, state, r.Event) case *pb.AgentStreamResponse_SystemMessage: - slog.Debug("Agent system message", "sessionID", sessionID, "message", r.SystemMessage.Message) - return nil + return m.handleAgentSystemMessage(ctx, sessionID, state, r.SystemMessage) case *pb.AgentStreamResponse_Result: return m.handleAgentResult(ctx, sessionID, state, r.Result) case *pb.AgentStreamResponse_Error: @@ -39,6 +39,34 @@ func (m *Manager) HandleAgentResponse(ctx context.Context, sessionID string, res } } +// handleAgentSystemMessage processes system-level messages from agent execution. +func (m *Manager) handleAgentSystemMessage(ctx context.Context, sessionID string, state *SessionState, systemMessage *pb.AgentSystemMessage) error { + if systemMessage == nil { + return nil + } + + trimmed := strings.TrimSpace(systemMessage.Message) + slog.Debug("Agent system message", "sessionID", sessionID, "message", trimmed) + + if !strings.EqualFold(trimmed, "interrupted") { + return nil + } + + // Treat interrupt as a terminal state for the prompt lifecycle. + m.emitAgentDone(ctx, sessionID) + m.updateSessionStatus(ctx, sessionID, pb.SessionStatus_SESSION_STATUS_READY) + m.updateLastActiveAt(ctx, sessionID) + + // Reset streaming state so a new prompt can start cleanly. + m.mu.Lock() + state.CurrentMessageID = "" + state.ContentBuilder.Reset() + state.OriginalPrompt = "" + m.mu.Unlock() + + return nil +} + // handleTextDelta processes text delta from agent streaming. func (m *Manager) handleTextDelta(ctx context.Context, sessionID string, state *SessionState, delta *pb.AgentTextDelta) error { m.mu.Lock() diff --git a/services/control-plane/internal/session/codex_auth_backend.go b/services/control-plane/internal/session/codex_auth_backend.go new file mode 100644 index 00000000..f3de5979 --- /dev/null +++ b/services/control-plane/internal/session/codex_auth_backend.go @@ -0,0 +1,353 @@ +package session + +import ( + "bytes" + "context" + "encoding/base64" + "encoding/json" + "fmt" + "io" + "log/slog" + "net/http" + "net/url" + "strings" + "time" + + pb "github.com/angristan/netclode/services/control-plane/gen/netclode/v1" + "github.com/angristan/netclode/services/control-plane/internal/storage" + "github.com/google/uuid" + "google.golang.org/protobuf/types/known/timestamppb" +) + +const ( + codexOAuthBaseURL = "https://auth.openai.com" + codexOAuthFlowTTL = 15 * time.Minute + codexOAuthHTTPTimeout = 15 * time.Second +) + +type codexAuthPendingState struct { + id string + deviceAuthID string + userCode string + verificationURI string + verificationURIComplete string + intervalSeconds int32 + expiresAt time.Time +} + +type codexDeviceCodeResponse struct { + DeviceAuthID string `json:"device_auth_id"` + UserCode string `json:"user_code"` + Interval json.Number `json:"interval"` +} + +type codexCodeExchange struct { + AuthorizationCode string `json:"authorization_code"` + CodeVerifier string `json:"code_verifier"` +} + +type codexTokenExchangeResponse struct { + AccessToken string `json:"access_token"` + RefreshToken string `json:"refresh_token"` + IdToken string `json:"id_token"` + ExpiresIn int64 `json:"expires_in,omitempty"` +} + +func (m *Manager) StartCodexAuth(ctx context.Context) (*pb.CodexAuthStartedResponse, error) { + now := time.Now().UTC() + if len(m.config.CodexOAuthEncryptionKey) != 32 { + return nil, fmt.Errorf("codex oauth is not configured: CODEX_OAUTH_ENCRYPTION_KEY_B64 must decode to 32 bytes") + } + + m.codexAuthMu.Lock() + if p := m.codexAuthPending; p != nil && now.Before(p.expiresAt) { + resp := &pb.CodexAuthStartedResponse{ + VerificationUri: p.verificationURI, + VerificationUriComplete: strPtr(p.verificationURIComplete), + UserCode: p.userCode, + IntervalSeconds: p.intervalSeconds, + ExpiresAt: timestamppb.New(p.expiresAt), + } + m.codexAuthMu.Unlock() + return resp, nil + } + m.codexAuthMu.Unlock() + + deviceCode, err := requestCodexDeviceCode(ctx) + if err != nil { + return nil, err + } + + intervalSeconds, _ := deviceCode.Interval.Int64() + if intervalSeconds <= 0 { + intervalSeconds = 5 + } + + pending := &codexAuthPendingState{ + id: uuid.NewString(), + deviceAuthID: deviceCode.DeviceAuthID, + userCode: deviceCode.UserCode, + verificationURI: codexOAuthBaseURL + "/codex/device", + verificationURIComplete: "", + intervalSeconds: int32(intervalSeconds), + expiresAt: now.Add(codexOAuthFlowTTL), + } + + m.codexAuthMu.Lock() + m.codexAuthPending = pending + m.codexAuthLastError = "" + m.codexAuthLastErrorAt = time.Time{} + m.codexAuthMu.Unlock() + + go m.finishCodexAuthFlow(pending) + + return &pb.CodexAuthStartedResponse{ + VerificationUri: pending.verificationURI, + VerificationUriComplete: strPtr(pending.verificationURIComplete), + UserCode: pending.userCode, + IntervalSeconds: pending.intervalSeconds, + ExpiresAt: timestamppb.New(pending.expiresAt), + }, nil +} + +func (m *Manager) finishCodexAuthFlow(pending *codexAuthPendingState) { + ctx, cancel := context.WithDeadline(context.Background(), pending.expiresAt) + defer cancel() + + exchange, err := pollCodexAuthorization(ctx, pending.deviceAuthID, pending.userCode, time.Duration(pending.intervalSeconds)*time.Second) + if err != nil { + m.failCodexAuthFlow(pending.id, err.Error()) + return + } + if !m.isCodexAuthFlowCurrent(pending.id) { + return + } + + tokens, err := exchangeCodexAuthCode(ctx, exchange) + if err != nil { + m.failCodexAuthFlow(pending.id, err.Error()) + return + } + if !m.isCodexAuthFlowCurrent(pending.id) { + return + } + + now := time.Now().UTC() + data := &storage.CodexOAuthSessionData{ + AccessToken: tokens.AccessToken, + IdToken: tokens.IdToken, + RefreshToken: tokens.RefreshToken, + ExpiresAt: inferCodexTokenExpiry(tokens.AccessToken, tokens.IdToken, tokens.ExpiresIn, now), + UpdatedAt: now, + } + if err := m.saveCodexOAuth(context.Background(), data); err != nil { + m.failCodexAuthFlow(pending.id, err.Error()) + return + } + + m.codexAuthMu.Lock() + if m.codexAuthPending != nil && m.codexAuthPending.id == pending.id { + m.codexAuthPending = nil + m.codexAuthLastError = "" + m.codexAuthLastErrorAt = time.Time{} + } + m.codexAuthMu.Unlock() + slog.Info("Codex OAuth authentication completed") +} + +func (m *Manager) isCodexAuthFlowCurrent(flowID string) bool { + m.codexAuthMu.Lock() + defer m.codexAuthMu.Unlock() + return m.codexAuthPending != nil && m.codexAuthPending.id == flowID +} + +func (m *Manager) failCodexAuthFlow(flowID, message string) { + m.codexAuthMu.Lock() + defer m.codexAuthMu.Unlock() + if m.codexAuthPending == nil || m.codexAuthPending.id != flowID { + return + } + m.codexAuthPending = nil + m.codexAuthLastError = message + m.codexAuthLastErrorAt = time.Now().UTC() + slog.Warn("Codex OAuth authentication failed", "error", message) +} + +func (m *Manager) GetCodexAuthStatus(ctx context.Context) (*pb.CodexAuthStatusResponse, error) { + now := time.Now().UTC() + + m.codexAuthMu.Lock() + if p := m.codexAuthPending; p != nil { + if now.Before(p.expiresAt) { + resp := &pb.CodexAuthStatusResponse{ + State: pb.CodexAuthState_CODEX_AUTH_STATE_PENDING, + ExpiresAt: timestamppb.New(p.expiresAt), + } + m.codexAuthMu.Unlock() + return resp, nil + } + m.codexAuthPending = nil + m.codexAuthLastError = "authorization timed out" + m.codexAuthLastErrorAt = now + } + lastErr := m.codexAuthLastError + m.codexAuthMu.Unlock() + + data, err := m.getCodexOAuth(ctx) + if err != nil { + return nil, err + } + if data != nil && data.AccessToken != "" && data.IdToken != "" && data.RefreshToken != "" { + resp := &pb.CodexAuthStatusResponse{ + State: pb.CodexAuthState_CODEX_AUTH_STATE_READY, + } + if data.ExpiresAt != nil { + resp.ExpiresAt = timestamppb.New(*data.ExpiresAt) + } + if accountID := codexAccountIDFromIDToken(data.IdToken); accountID != "" { + resp.AccountId = &accountID + } + return resp, nil + } + + if lastErr != "" { + return &pb.CodexAuthStatusResponse{ + State: pb.CodexAuthState_CODEX_AUTH_STATE_ERROR, + Error: &lastErr, + }, nil + } + + return &pb.CodexAuthStatusResponse{ + State: pb.CodexAuthState_CODEX_AUTH_STATE_UNAUTHENTICATED, + }, nil +} + +func (m *Manager) LogoutCodexAuth(ctx context.Context) error { + m.codexAuthMu.Lock() + m.codexAuthPending = nil + m.codexAuthLastError = "" + m.codexAuthLastErrorAt = time.Time{} + m.codexAuthMu.Unlock() + return m.storage.DeleteCodexOAuth(ctx) +} + +func requestCodexDeviceCode(ctx context.Context) (*codexDeviceCodeResponse, error) { + payload, _ := json.Marshal(map[string]string{"client_id": codexOAuthClientID}) + req, err := http.NewRequestWithContext(ctx, http.MethodPost, codexOAuthBaseURL+"/api/accounts/deviceauth/usercode", bytes.NewReader(payload)) + if err != nil { + return nil, err + } + req.Header.Set("Content-Type", "application/json") + + resp, err := (&http.Client{Timeout: codexOAuthHTTPTimeout}).Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + body, _ := io.ReadAll(io.LimitReader(resp.Body, 2048)) + return nil, fmt.Errorf("request device code failed: status=%d body=%s", resp.StatusCode, string(body)) + } + var out codexDeviceCodeResponse + if err := json.NewDecoder(resp.Body).Decode(&out); err != nil { + return nil, err + } + return &out, nil +} + +func pollCodexAuthorization(ctx context.Context, deviceAuthID, userCode string, interval time.Duration) (*codexCodeExchange, error) { + if interval <= 0 { + interval = 5 * time.Second + } + + client := &http.Client{Timeout: codexOAuthHTTPTimeout} + for { + select { + case <-ctx.Done(): + return nil, ctx.Err() + case <-time.After(interval): + } + + payload, _ := json.Marshal(map[string]string{ + "device_auth_id": deviceAuthID, + "user_code": userCode, + }) + req, err := http.NewRequestWithContext(ctx, http.MethodPost, codexOAuthBaseURL+"/api/accounts/deviceauth/token", bytes.NewReader(payload)) + if err != nil { + return nil, err + } + req.Header.Set("Content-Type", "application/json") + + resp, err := client.Do(req) + if err != nil { + return nil, err + } + if resp.StatusCode == http.StatusForbidden || resp.StatusCode == http.StatusNotFound { + resp.Body.Close() + continue + } + if resp.StatusCode != http.StatusOK { + body, _ := io.ReadAll(io.LimitReader(resp.Body, 2048)) + resp.Body.Close() + return nil, fmt.Errorf("device authorization failed: status=%d body=%s", resp.StatusCode, string(body)) + } + var out codexCodeExchange + err = json.NewDecoder(resp.Body).Decode(&out) + resp.Body.Close() + return &out, err + } +} + +func exchangeCodexAuthCode(ctx context.Context, exchange *codexCodeExchange) (*codexTokenExchangeResponse, error) { + form := url.Values{ + "grant_type": {"authorization_code"}, + "code": {exchange.AuthorizationCode}, + "redirect_uri": {"https://auth.openai.com/deviceauth/callback"}, + "client_id": {codexOAuthClientID}, + "code_verifier": {exchange.CodeVerifier}, + } + req, err := http.NewRequestWithContext(ctx, http.MethodPost, codexOAuthBaseURL+"/oauth/token", strings.NewReader(form.Encode())) + if err != nil { + return nil, err + } + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + + resp, err := (&http.Client{Timeout: codexOAuthHTTPTimeout}).Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + body, _ := io.ReadAll(io.LimitReader(resp.Body, 2048)) + return nil, fmt.Errorf("token exchange failed: status=%d body=%s", resp.StatusCode, string(body)) + } + + var out codexTokenExchangeResponse + if err := json.NewDecoder(resp.Body).Decode(&out); err != nil { + return nil, err + } + if out.AccessToken == "" || out.IdToken == "" || out.RefreshToken == "" { + return nil, fmt.Errorf("token exchange response missing required tokens") + } + return &out, nil +} + +func codexAccountIDFromIDToken(idToken string) string { + parts := strings.Split(idToken, ".") + if len(parts) != 3 { + return "" + } + payload, err := base64.RawURLEncoding.DecodeString(parts[1]) + if err != nil { + return "" + } + var claims struct { + Auth struct { + ChatGPTAccountID string `json:"chatgpt_account_id"` + } `json:"https://api.openai.com/auth"` + } + if err := json.Unmarshal(payload, &claims); err != nil { + return "" + } + return claims.Auth.ChatGPTAccountID +} diff --git a/services/control-plane/internal/session/codex_oauth.go b/services/control-plane/internal/session/codex_oauth.go new file mode 100644 index 00000000..50f0c0b0 --- /dev/null +++ b/services/control-plane/internal/session/codex_oauth.go @@ -0,0 +1,169 @@ +package session + +import ( + "context" + "encoding/base64" + "encoding/json" + "fmt" + "io" + "log/slog" + "net/http" + "net/url" + "os" + "strings" + "time" + + "github.com/angristan/netclode/services/control-plane/internal/storage" +) + +const ( + codexOAuthClientID = "app_EMoamEEZ73f0CkXaXp7hrann" + codexOAuthTokenEndpoint = "https://auth.openai.com/oauth/token" + codexRefreshLeadTime = 12 * time.Hour + codexRefreshURLOverride = "CODEX_REFRESH_TOKEN_URL_OVERRIDE" +) + +type codexRefreshResponse struct { + AccessToken string `json:"access_token"` + RefreshToken string `json:"refresh_token"` + IdToken string `json:"id_token"` + ExpiresIn int64 `json:"expires_in"` +} + +func (m *Manager) prepareCodexOAuthForPrompt(ctx context.Context, sessionID string) (*storage.CodexOAuthSessionData, error) { + data, err := m.getCodexOAuth(ctx) + if err != nil { + return nil, err + } + if data == nil { + return nil, fmt.Errorf("missing codex oauth data") + } + if data.AccessToken == "" || data.IdToken == "" { + return nil, fmt.Errorf("incomplete codex oauth data") + } + + now := time.Now().UTC() + if data.ExpiresAt == nil { + if inferred := inferCodexTokenExpiry(data.AccessToken, data.IdToken, 0, now); inferred != nil { + data.ExpiresAt = inferred + data.UpdatedAt = now + if err := m.saveCodexOAuth(ctx, data); err != nil { + return nil, fmt.Errorf("save inferred token expiry: %w", err) + } + } + } + + if shouldRefreshCodexOAuth(data, now) { + if data.RefreshToken == "" { + return nil, fmt.Errorf("oauth refresh required but refresh token is missing") + } + + slog.Info("Refreshing Codex OAuth tokens", "sessionID", sessionID) + refreshed, err := refreshCodexOAuth(ctx, data.RefreshToken) + if err != nil { + slog.Warn("Codex OAuth refresh failed", "sessionID", sessionID, "error", err) + return nil, err + } + if refreshed.AccessToken != "" { + data.AccessToken = refreshed.AccessToken + } + if refreshed.IdToken != "" { + data.IdToken = refreshed.IdToken + } + if refreshed.RefreshToken != "" { + data.RefreshToken = refreshed.RefreshToken + } + if data.AccessToken == "" || data.IdToken == "" { + return nil, fmt.Errorf("refresh response missing required access/id token") + } + + data.ExpiresAt = inferCodexTokenExpiry(data.AccessToken, data.IdToken, refreshed.ExpiresIn, now) + data.UpdatedAt = now + if err := m.saveCodexOAuth(ctx, data); err != nil { + return nil, fmt.Errorf("save refreshed oauth data: %w", err) + } + slog.Info("Codex OAuth refresh succeeded", "sessionID", sessionID) + } + + return data, nil +} + +func shouldRefreshCodexOAuth(data *storage.CodexOAuthSessionData, now time.Time) bool { + if data == nil || data.ExpiresAt == nil { + return true + } + return now.Add(codexRefreshLeadTime).After(data.ExpiresAt.UTC()) +} + +func refreshCodexOAuth(ctx context.Context, refreshToken string) (*codexRefreshResponse, error) { + form := url.Values{ + "grant_type": {"refresh_token"}, + "client_id": {codexOAuthClientID}, + "refresh_token": {refreshToken}, + } + + endpoint := os.Getenv(codexRefreshURLOverride) + // Backward-compatible alias for older tests/config. + if endpoint == "" { + endpoint = os.Getenv("CODEX_OAUTH_TOKEN_URL_OVERRIDE") + } + if endpoint == "" { + endpoint = codexOAuthTokenEndpoint + } + req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, strings.NewReader(form.Encode())) + if err != nil { + return nil, err + } + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + + client := &http.Client{Timeout: 15 * time.Second} + resp, err := client.Do(req) + if err != nil { + return nil, fmt.Errorf("refresh token request failed: %w", err) + } + defer resp.Body.Close() + + body, _ := io.ReadAll(io.LimitReader(resp.Body, 4096)) + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("refresh token failed with status %d", resp.StatusCode) + } + + var refreshed codexRefreshResponse + if err := json.Unmarshal(body, &refreshed); err != nil { + return nil, fmt.Errorf("decode refresh response: %w", err) + } + return &refreshed, nil +} + +func inferCodexTokenExpiry(accessToken, idToken string, expiresIn int64, now time.Time) *time.Time { + if expiresIn > 0 { + t := now.Add(time.Duration(expiresIn) * time.Second).UTC() + return &t + } + if t := parseJWTExp(accessToken); t != nil { + return t + } + if t := parseJWTExp(idToken); t != nil { + return t + } + return nil +} + +func parseJWTExp(token string) *time.Time { + parts := strings.Split(token, ".") + if len(parts) != 3 { + return nil + } + payload, err := base64.RawURLEncoding.DecodeString(parts[1]) + if err != nil { + return nil + } + var claims struct { + Exp int64 `json:"exp"` + } + if err := json.Unmarshal(payload, &claims); err != nil || claims.Exp <= 0 { + return nil + } + t := time.Unix(claims.Exp, 0).UTC() + return &t +} diff --git a/services/control-plane/internal/session/manager.go b/services/control-plane/internal/session/manager.go index 592f2d4b..ab04d8d1 100644 --- a/services/control-plane/internal/session/manager.go +++ b/services/control-plane/internal/session/manager.go @@ -18,6 +18,7 @@ import ( "github.com/google/uuid" "google.golang.org/protobuf/encoding/protojson" "google.golang.org/protobuf/types/known/timestamppb" + k8serrors "k8s.io/apimachinery/pkg/api/errors" ) const ( @@ -30,18 +31,22 @@ type SessionUpdateCallback func(session *pb.Session) // AgentSessionConfig contains typed configuration for an agent session. type AgentSessionConfig struct { - SessionID string - GitHubToken string // For git credentials (from GitHub App) - not proxied, used in git URLs - Repos []string - RepoAccess *pb.RepoAccess - SdkType *pb.SdkType - Model string - CopilotBackend *pb.CopilotBackend - CodexAccessToken string // For Codex OAuth mode - written to ~/.codex/auth.json, can't be proxied - CodexIdToken string // For Codex OAuth mode - written to ~/.codex/auth.json, can't be proxied - CodexRefreshToken string // For Codex OAuth mode - written to ~/.codex/auth.json, can't be proxied - ReasoningEffort string // For Codex reasoning effort (low, medium, high) - OllamaURL string // For local Ollama inference + SessionID string + OpenAIAPIKey string // For Codex API mode + MistralAPIKey string // For OpenCode Mistral models + GitHubToken string // For git credentials (from GitHub App) + GitHubCopilotToken string // For Copilot SDK + Repos []string + RepoAccess *pb.RepoAccess + SdkType *pb.SdkType + Model string + CopilotBackend *pb.CopilotBackend + CodexAccessToken string // For Codex OAuth mode + CodexIdToken string // For Codex OAuth mode + ReasoningEffort string // For Codex reasoning effort (low, medium, high) + OllamaURL string // For local Ollama inference + OpenCodeAPIKey string // For OpenCode Zen models + ZaiAPIKey string // For Z.AI GLM-4.7 models } // AgentConnection represents a connected agent that can receive commands. @@ -54,6 +59,7 @@ type AgentConnection interface { SendTerminalInput(data string) error ResizeTerminal(cols, rows int) error UpdateGitCredentials(token string, repoAccess pb.RepoAccess) error + UpdateCodexAuth(accessToken, idToken string, expiresAt *timestamppb.Timestamp) error } // WarmAgentConnection extends AgentConnection with session assignment capability. @@ -77,6 +83,12 @@ type Manager struct { // onSessionUpdated is called when a session is updated internally (e.g., auto-pause). onSessionUpdated SessionUpdateCallback + + // Backend-managed Codex OAuth device flow state. + codexAuthMu sync.Mutex + codexAuthPending *codexAuthPendingState + codexAuthLastError string + codexAuthLastErrorAt time.Time } // NewManager creates a new session manager. @@ -268,6 +280,20 @@ func (m *Manager) Create(ctx context.Context, name string, repos []string, repoA } } + // Codex OAuth sessions require backend-managed global OAuth credentials. + if sdkType != nil && *sdkType == pb.SdkType_SDK_TYPE_CODEX && model != nil { + authMode, _ := parseCodexAuthModeAndEffort(*model) + if authMode == "oauth" { + oauthData, err := m.getCodexOAuth(ctx) + if err != nil { + return nil, fmt.Errorf("get codex oauth data: %w", err) + } + if oauthData == nil || oauthData.AccessToken == "" || oauthData.IdToken == "" || oauthData.RefreshToken == "" { + return nil, fmt.Errorf("codex oauth is not configured") + } + } + } + // Ensure we have a slot for a new active session m.ensureActiveSlot(ctx, "") @@ -370,69 +396,76 @@ func (m *Manager) createSandboxDirect(ctx context.Context, sessionID string, rep "ANTHROPIC_API_KEY": m.config.AnthropicAPIKey, } - // If restoring from snapshot, create the PVC first and wait for restore to complete - // BEFORE creating the sandbox. This ensures the restore job finishes before the pod mounts. - if snapID != "" { - slog.Info("Creating PVC from snapshot before sandbox", "sessionID", sessionID, "snapshotID", snapID) - - // Create standalone PVC from snapshot - pvcName, err := m.k8s.CreatePVCFromSnapshot(ctx, sessionID, snapID) - if err != nil { - slog.Error("Failed to create PVC from snapshot", "sessionID", sessionID, "error", err) - m.updateSessionStatus(ctx, sessionID, pb.SessionStatus_SESSION_STATUS_ERROR) - m.emitSessionError(ctx, sessionID, fmt.Sprintf("failed to create PVC from snapshot: %v", err)) - return - } - - // Wait for JuiceFS restore job to complete BEFORE creating sandbox. - // We must wait because the pod will fail with I/O errors if it tries to access - // the filesystem before the restore completes. - slog.Info("Waiting for snapshot restore job", "sessionID", sessionID, "snapshotID", snapID) - if err := m.k8s.WaitForRestoreJob(ctx, sessionID, snapID, 5*time.Minute); err != nil { - slog.Error("Snapshot restore job failed", "sessionID", sessionID, "error", err) - // Cleanup: delete the PVC we created - if delErr := m.k8s.DeletePVC(ctx, sessionID); delErr != nil { - slog.Error("Failed to cleanup PVC after restore failure", "sessionID", sessionID, "error", delErr) - } - m.updateSessionStatus(ctx, sessionID, pb.SessionStatus_SESSION_STATUS_ERROR) - m.emitSessionError(ctx, sessionID, fmt.Sprintf("snapshot restore failed: %v", err)) - return - } - slog.Info("Snapshot restore completed, creating sandbox with existing PVC", "sessionID", sessionID, "pvc", pvcName) + // If no explicit restore was requested, validate stored resume PVC. + if snapID == "" { + // Not restoring from snapshot - check if we have an existing PVC (resume after pause) + if existingPVC, err := m.storage.GetPVCName(ctx, sessionID); err == nil && existingPVC != "" { + exists, checkErr := m.k8s.PVCExists(ctx, existingPVC) + switch { + case checkErr != nil: + // If we cannot validate, keep previous behavior to avoid unexpected data loss. + slog.Warn("Failed to validate stored PVC, attempting resume with stored PVC", "sessionID", sessionID, "pvc", existingPVC, "error", checkErr) + env[k8s.ExistingPVCEnvKey] = existingPVC + case exists: + slog.Info("Resuming with existing PVC", "sessionID", sessionID, "pvc", existingPVC) + env[k8s.ExistingPVCEnvKey] = existingPVC + default: + // Stale Redis state can point to a PVC that no longer exists. + // First, try the canonical direct-mode PVC name for this session. + // This handles sessions where Redis points to an old warm-pool PVC but the + // direct-mode PVC still exists with the user's workspace. + slog.Warn("Stored PVC not found, attempting latest snapshot restore", "sessionID", sessionID, "pvc", existingPVC) + if err := m.storage.SetPVCName(ctx, sessionID, ""); err != nil { + slog.Warn("Failed to clear stale PVC name from storage", "sessionID", sessionID, "pvc", existingPVC, "error", err) + } - // Ensure the session anchor ConfigMap exists and owns the new PVC. - // This prevents the PVC from being garbage-collected if the sandbox fails or is paused. - if err := m.k8s.EnsureSessionAnchor(ctx, sessionID); err != nil { - slog.Warn("Failed to create session anchor", "sessionID", sessionID, "error", err) - } else if err := m.k8s.AddSessionAnchorToPVC(ctx, sessionID, pvcName); err != nil { - slog.Warn("Failed to add session anchor to PVC", "sessionID", sessionID, "pvc", pvcName, "error", err) - } + canonicalPVC := fmt.Sprintf("agent-home-sess-%s", sessionID) + canonicalExists, canonicalErr := m.k8s.PVCExists(ctx, canonicalPVC) + if canonicalErr != nil { + slog.Warn("Failed to validate canonical session PVC", "sessionID", sessionID, "pvc", canonicalPVC, "error", canonicalErr) + } - // Delete the old orphaned PVC in background (non-blocking) - // Only if it's different from the new PVC (avoids deleting the newly created one) - go func(sessionID, newPVCName string) { - bgCtx := context.Background() - if oldPVCName, err := m.storage.GetOldPVCName(bgCtx, sessionID); err == nil && oldPVCName != "" { - if oldPVCName == newPVCName { - slog.Info("Skipping old PVC deletion (same as new PVC)", "sessionID", sessionID, "pvc", oldPVCName) + if canonicalExists { + slog.Warn("Stored PVC missing, resuming with canonical session PVC", "sessionID", sessionID, "pvc", canonicalPVC) + env[k8s.ExistingPVCEnvKey] = canonicalPVC + if err := m.storage.SetPVCName(ctx, sessionID, canonicalPVC); err != nil { + slog.Warn("Failed to persist canonical PVC name", "sessionID", sessionID, "pvc", canonicalPVC, "error", err) + } } else { - if err := m.k8s.DeletePVCByName(bgCtx, oldPVCName); err != nil { - slog.Warn("Failed to delete old PVC after restore", "sessionID", sessionID, "pvc", oldPVCName, "error", err) + // Canonical PVC is also missing, restore from latest snapshot. + snapshots, snapErr := m.storage.ListSnapshots(ctx, sessionID) + if snapErr != nil { + slog.Warn("Failed to list snapshots for missing PVC recovery", "sessionID", sessionID, "error", snapErr) + } else if len(snapshots) > 0 && snapshots[0] != nil && snapshots[0].Id != "" { + snapID = snapshots[0].Id // ListSnapshots returns newest-first. + slog.Warn("Resuming from latest snapshot after missing PVC", "sessionID", sessionID, "snapshotID", snapID) } else { - slog.Info("Deleted old PVC after restore", "sessionID", sessionID, "pvc", oldPVCName) + slog.Warn("No snapshots available for missing PVC recovery; creating fresh PVC", "sessionID", sessionID) } } - _ = m.storage.ClearOldPVCName(bgCtx, sessionID) } - }(sessionID, pvcName) + } + } - // Pass the existing PVC name so sandbox uses it instead of creating a new one - env[k8s.ExistingPVCEnvKey] = pvcName - } else { - // Not restoring from snapshot - check if we have an existing PVC (resume after pause) - if existingPVC, err := m.storage.GetPVCName(ctx, sessionID); err == nil && existingPVC != "" { - slog.Info("Resuming with existing PVC", "sessionID", sessionID, "pvc", existingPVC) - env[k8s.ExistingPVCEnvKey] = existingPVC + // If restoring from snapshot, create the PVC first and wait for restore to complete + // BEFORE creating the sandbox. This ensures the restore job finishes before the pod mounts. + if snapID != "" { + slog.Info("Creating PVC from snapshot before sandbox", "sessionID", sessionID, "snapshotID", snapID) + pvcName, err := m.preparePVCFromSnapshot(ctx, sessionID, snapID) + if err != nil { + // For explicit user-driven restore, fail hard. + // For automatic missing-PVC recovery, continue with fresh PVC. + if len(restoreSnapshotID) > 0 { + slog.Error("Failed to restore from snapshot", "sessionID", sessionID, "snapshotID", snapID, "error", err) + m.updateSessionStatus(ctx, sessionID, pb.SessionStatus_SESSION_STATUS_ERROR) + m.emitSessionError(ctx, sessionID, fmt.Sprintf("snapshot restore failed: %v", err)) + return + } + slog.Warn("Auto-restore from latest snapshot failed, creating sandbox with fresh PVC", "sessionID", sessionID, "snapshotID", snapID, "error", err) + } else { + slog.Info("Snapshot restore completed, creating sandbox with existing PVC", "sessionID", sessionID, "pvc", pvcName) + // Pass the existing PVC name so sandbox uses it instead of creating a new one. + env[k8s.ExistingPVCEnvKey] = pvcName } } @@ -571,6 +604,51 @@ func (m *Manager) createSandboxDirect(ctx context.Context, sessionID string, rep slog.Info("Session sandbox ready", "sessionID", sessionID, "fqdn", fqdn, "status", newStatus) } +func (m *Manager) preparePVCFromSnapshot(ctx context.Context, sessionID, snapshotID string) (string, error) { + // Create standalone PVC from snapshot. + pvcName, err := m.k8s.CreatePVCFromSnapshot(ctx, sessionID, snapshotID) + if err != nil { + return "", fmt.Errorf("create PVC from snapshot: %w", err) + } + + // Wait for JuiceFS restore job to complete BEFORE creating sandbox. + if err := m.k8s.WaitForRestoreJob(ctx, sessionID, snapshotID, 5*time.Minute); err != nil { + // Cleanup: delete the PVC we created. + if delErr := m.k8s.DeletePVC(ctx, sessionID); delErr != nil { + slog.Error("Failed to cleanup PVC after restore failure", "sessionID", sessionID, "error", delErr) + } + return "", fmt.Errorf("wait for restore job: %w", err) + } + + // Ensure the session anchor ConfigMap exists and owns the new PVC. + // This prevents the PVC from being garbage-collected if the sandbox fails or is paused. + if err := m.k8s.EnsureSessionAnchor(ctx, sessionID); err != nil { + slog.Warn("Failed to create session anchor", "sessionID", sessionID, "error", err) + } else if err := m.k8s.AddSessionAnchorToPVC(ctx, sessionID, pvcName); err != nil { + slog.Warn("Failed to add session anchor to PVC", "sessionID", sessionID, "pvc", pvcName, "error", err) + } + + // Delete the old orphaned PVC in background (non-blocking) + // Only if it's different from the new PVC (avoids deleting the newly created one) + go func(sessionID, newPVCName string) { + bgCtx := context.Background() + if oldPVCName, err := m.storage.GetOldPVCName(bgCtx, sessionID); err == nil && oldPVCName != "" { + if oldPVCName == newPVCName { + slog.Info("Skipping old PVC deletion (same as new PVC)", "sessionID", sessionID, "pvc", oldPVCName) + } else { + if err := m.k8s.DeletePVCByName(bgCtx, oldPVCName); err != nil { + slog.Warn("Failed to delete old PVC after restore", "sessionID", sessionID, "pvc", oldPVCName, "error", err) + } else { + slog.Info("Deleted old PVC after restore", "sessionID", sessionID, "pvc", oldPVCName) + } + } + _ = m.storage.ClearOldPVCName(bgCtx, sessionID) + } + }(sessionID, pvcName) + + return pvcName, nil +} + // createSandboxViaClaim uses SandboxClaim for warm pool allocation func (m *Manager) createSandboxViaClaim(ctx context.Context, sessionID string, repos []string, repoAccess *pb.RepoAccess, tailnetEnabled bool) { // Always use the same template to leverage the warm pool @@ -1069,7 +1147,12 @@ func (m *Manager) ExposePort(ctx context.Context, sessionID string, port int) (s return "", err } - previewURL := fmt.Sprintf("http://sandbox-%s:%d", sessionID, port) + previewHost, err := m.k8s.GetSandboxPreviewHostname(ctx, sessionID) + if err != nil { + slog.Warn("Failed to resolve sandbox preview hostname, using fallback", "sessionID", sessionID, "error", err) + previewHost = fmt.Sprintf("sandbox-%s", sessionID) + } + previewURL := fmt.Sprintf("http://%s:%d", previewHost, port) port32 := int32(port) // Create and persist the port_exposed event @@ -1091,9 +1174,36 @@ func (m *Manager) ExposePort(ctx context.Context, sessionID string, port int) (s return previewURL, nil } -// restoreExposedPorts re-exposes ports that were previously exposed for a session. +// UnexposePort removes a port exposure for a session via Tailscale and persists the event. +func (m *Manager) UnexposePort(ctx context.Context, sessionID string, port int) error { + if err := m.k8s.UnexposePort(ctx, sessionID, port); err != nil { + if !k8serrors.IsNotFound(err) { + return err + } + slog.Info("Unexpose target not found, persisting event anyway", "sessionID", sessionID, "port", port, "error", err) + } + + port32 := int32(port) + event := &pb.AgentEvent{ + Kind: pb.AgentEventKind_AGENT_EVENT_KIND_PORT_UNEXPOSED, + CorrelationId: fmt.Sprintf("port-%d", port), + Payload: &pb.AgentEvent_PortUnexposed{ + PortUnexposed: &pb.PortUnexposedPayload{ + Port: port32, + }, + }, + } + + // Emit to all connected clients (this also persists to the stream) + // Port unexposed events are final (not partial streaming) + m.emitAgentEvent(ctx, sessionID, event, false) + + return nil +} + +// restoreExposedPorts re-exposes ports that are currently exposed for a session. // This is called during resume to restore port exposure after sandbox recreation. -// It reads persisted port_exposed events from the stream and re-applies them to K8s. +// It reads persisted port_exposed/port_unexposed events from the stream and re-applies final state to K8s. func (m *Manager) restoreExposedPorts(ctx context.Context, sessionID string) { // Read all event entries from the stream entries, err := m.storage.GetStreamEntriesByTypes(ctx, sessionID, "0", 0, []string{storage.StreamEntryTypeEvent}) @@ -1102,20 +1212,25 @@ func (m *Manager) restoreExposedPorts(ctx context.Context, sessionID string) { return } - // Collect unique ports that were exposed + // Rebuild final port exposure state from persisted events. exposedPorts := make(map[int32]bool) for _, e := range entries { if e.Entry.Partial { continue // Skip partial (streaming) entries } var event pb.AgentEvent - if err := json.Unmarshal(e.Entry.Payload, &event); err != nil { + if err := protojson.Unmarshal(e.Entry.Payload, &event); err != nil { continue } - if event.Kind == pb.AgentEventKind_AGENT_EVENT_KIND_PORT_EXPOSED { + switch event.Kind { + case pb.AgentEventKind_AGENT_EVENT_KIND_PORT_EXPOSED: if portPayload := event.GetPortExposed(); portPayload != nil { exposedPorts[portPayload.Port] = true } + case pb.AgentEventKind_AGENT_EVENT_KIND_PORT_UNEXPOSED: + if portPayload := event.GetPortUnexposed(); portPayload != nil { + delete(exposedPorts, portPayload.Port) + } } } @@ -1522,10 +1637,15 @@ func (m *Manager) GetSessionConfig(ctx context.Context, sessionID string) (*Agen } config := &AgentSessionConfig{ - SessionID: sessionID, - SdkType: state.Session.SdkType, - CopilotBackend: state.Session.CopilotBackend, - OllamaURL: m.config.OllamaURL, + SessionID: sessionID, + OpenAIAPIKey: m.config.OpenAIAPIKey, + MistralAPIKey: m.config.MistralAPIKey, + GitHubCopilotToken: m.config.GitHubCopilotToken, + SdkType: state.Session.SdkType, + CopilotBackend: state.Session.CopilotBackend, + OllamaURL: m.config.OllamaURL, + OpenCodeAPIKey: m.config.OpenCodeAPIKey, + ZaiAPIKey: m.config.ZaiAPIKey, } if state.Session.Model != nil { @@ -1555,27 +1675,30 @@ func (m *Manager) GetSessionConfig(ctx context.Context, sessionID string) (*Agen // For Codex SDK, parse model format: base:auth:effort (e.g., gpt-5-codex:oauth:high) // Also supports legacy format: base:auth (e.g., gpt-5-codex:oauth) if state.Session.SdkType != nil && *state.Session.SdkType == pb.SdkType_SDK_TYPE_CODEX { - parts := strings.Split(model, ":") - if len(parts) >= 2 { - authMode := parts[len(parts)-1] - // Check if last part is a reasoning effort level - if authMode == "low" || authMode == "medium" || authMode == "high" || authMode == "minimal" || authMode == "xhigh" { - config.ReasoningEffort = authMode - // Auth mode is second-to-last - if len(parts) >= 3 { - authMode = parts[len(parts)-2] - } else { - authMode = "" - } - } + authMode, reasoningEffort := parseCodexAuthModeAndEffort(model) + if authMode == "" { + return nil, fmt.Errorf("invalid codex model for session %s: missing auth suffix (:oauth or :api)", sessionID) + } + if reasoningEffort != "" { + config.ReasoningEffort = reasoningEffort + } - // For OAuth mode, send tokens (written to ~/.codex/auth.json, can't be proxied). - // API mode uses OPENAI_API_KEY placeholder env var, proxied by secret-proxy. - if authMode == "oauth" { - config.CodexAccessToken = m.config.CodexAccessToken - config.CodexIdToken = m.config.CodexIdToken - config.CodexRefreshToken = m.config.CodexRefreshToken + // Set credentials based on auth mode. + // OAuth mode is session-scoped and loaded from encrypted storage, never from global env. + if authMode == "api" { + config.OpenAIAPIKey = m.config.OpenAIAPIKey + } else if authMode == "oauth" { + // Ensure OAuth sessions do not carry API-key credentials. + config.OpenAIAPIKey = "" + oauthData, err := m.getSessionCodexOAuth(ctx, sessionID) + if err != nil { + return nil, fmt.Errorf("get session codex oauth data: %w", err) } + if oauthData == nil || oauthData.AccessToken == "" || oauthData.IdToken == "" { + return nil, fmt.Errorf("codex oauth tokens not configured for session %s", sessionID) + } + config.CodexAccessToken = oauthData.AccessToken + config.CodexIdToken = oauthData.IdToken } } } @@ -2097,11 +2220,9 @@ func (m *Manager) getAllowedSecretForHost(sdkType pb.SdkType, host string) (secr } case pb.SdkType_SDK_TYPE_CODEX: - allowedMappings = []hostMapping{ - // Codex API mode: agent sends OPENAI_API_KEY placeholder, proxy injects OAuth access token - // (OpenAI API accepts OAuth tokens as Bearer tokens) - {hosts: []string{"api.openai.com"}, secretKey: "codex_access", placeholder: "NETCLODE_PLACEHOLDER_openai"}, - } + // Codex authentication is now provided directly by backend-issued API/OAuth + // tokens to the agent. No placeholder replacement is configured for Codex. + allowedMappings = nil default: // Default to Claude behavior @@ -2129,6 +2250,79 @@ func (m *Manager) getAllowedSecretForHost(sdkType pb.SdkType, host string) (secr return "", "" } +func isCodexReasoningEffort(value string) bool { + switch value { + case "minimal", "low", "medium", "high", "xhigh": + return true + default: + return false + } +} + +// parseCodexAuthModeAndEffort extracts auth mode and reasoning effort from model suffix. +// Supported formats: +// - base:api +// - base:oauth +// - base:api:high +// - base:oauth:low +func parseCodexAuthModeAndEffort(model string) (authMode, reasoningEffort string) { + parts := strings.Split(model, ":") + if len(parts) < 2 { + return "", "" + } + + authMode = parts[len(parts)-1] + if isCodexReasoningEffort(authMode) { + reasoningEffort = authMode + if len(parts) >= 3 { + authMode = parts[len(parts)-2] + } else { + authMode = "" + } + } + + if authMode != "api" && authMode != "oauth" { + authMode = "" + } + return authMode, reasoningEffort +} + +func cloneCodexOAuthData(data *storage.CodexOAuthSessionData) *storage.CodexOAuthSessionData { + if data == nil { + return nil + } + c := *data + if data.ExpiresAt != nil { + expiresAt := *data.ExpiresAt + c.ExpiresAt = &expiresAt + } + return &c +} + +func (m *Manager) getCodexOAuth(ctx context.Context) (*storage.CodexOAuthSessionData, error) { + data, err := m.storage.GetCodexOAuth(ctx) + if err != nil || data == nil { + return data, err + } + return cloneCodexOAuthData(data), nil +} + +func (m *Manager) saveCodexOAuth(ctx context.Context, data *storage.CodexOAuthSessionData) error { + if err := m.storage.SaveCodexOAuth(ctx, data); err != nil { + return err + } + return nil +} + +// Legacy wrappers kept while tests/branches are still converging. +func (m *Manager) getSessionCodexOAuth(ctx context.Context, _ string) (*storage.CodexOAuthSessionData, error) { + return m.getCodexOAuth(ctx) +} + +func (m *Manager) saveSessionCodexOAuth(ctx context.Context, _ string, data *storage.CodexOAuthSessionData) error { + return m.saveCodexOAuth(ctx, data) +} + // ListModels returns available models for the specified SDK type. // For Copilot SDK, returns a combined list of GitHub Copilot and Anthropic (BYOK) models. // For Codex SDK, returns OpenAI models with "gpt-codex" family. @@ -2649,13 +2843,18 @@ func (m *Manager) getCopilotModelsFallback() []*pb.ModelInfo { } // fetchCodexModels fetches OpenAI Codex models (family: gpt-codex or gpt-codex-mini) -// Returns models with auth mode suffix based on available credentials: +// and returns auth mode suffix variants based on backend-available credentials: // - ":api" suffix when OPENAI_API_KEY is configured -// - ":oauth" suffix when CODEX_ACCESS_TOKEN is configured -// If both are configured, returns both sets of models +// - ":oauth" suffix when backend Codex OAuth credentials are configured +// If both are configured, both sets are returned. func (m *Manager) fetchCodexModels() []*pb.ModelInfo { hasAPIKey := m.config.OpenAIAPIKey != "" - hasOAuth := m.config.CodexAccessToken != "" + hasOAuth := false + if data, err := m.storage.GetCodexOAuth(context.Background()); err != nil { + slog.Warn("Failed to read global Codex OAuth credentials", "error", err) + } else if data != nil && data.AccessToken != "" && data.IdToken != "" && data.RefreshToken != "" { + hasOAuth = true + } // If neither is configured, return empty if !hasAPIKey && !hasOAuth { @@ -2801,7 +3000,7 @@ func (m *Manager) CreateSnapshot(ctx context.Context, sessionID string, name str continue // Skip partial (streaming) entries } var event pb.AgentEvent - if err := json.Unmarshal(e.Entry.Payload, &event); err == nil { + if err := protojson.Unmarshal(e.Entry.Payload, &event); err == nil { if event.Kind == pb.AgentEventKind_AGENT_EVENT_KIND_MESSAGE { messageEventCount++ } diff --git a/services/control-plane/internal/session/manager_test.go b/services/control-plane/internal/session/manager_test.go index df00ada7..606c8c79 100644 --- a/services/control-plane/internal/session/manager_test.go +++ b/services/control-plane/internal/session/manager_test.go @@ -2,6 +2,12 @@ package session import ( "context" + "encoding/json" + "errors" + "net/http" + "net/http/httptest" + "strconv" + "strings" "sync" "testing" "time" @@ -11,7 +17,10 @@ import ( "github.com/angristan/netclode/services/control-plane/internal/k8s" "github.com/angristan/netclode/services/control-plane/internal/storage" "github.com/redis/go-redis/v9" + "google.golang.org/protobuf/encoding/protojson" "google.golang.org/protobuf/types/known/timestamppb" + k8serrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime/schema" ) // mockRuntime implements k8s.Runtime for testing @@ -25,15 +34,26 @@ type mockRuntime struct { createdSandboxes []string createdClaims []string createdServices []string + exposedPorts map[string]map[int]bool labeledSandboxes map[string]string // sandboxName -> sessionID readyCallbacks map[string][]k8s.SandboxReadyCallback + createSandboxEnv map[string]map[string]string + pvcExists map[string]bool + restoreSnapshot []string + previewHostname string + previewHostErr error + unexposePortErr error } func newMockRuntime() *mockRuntime { return &mockRuntime{ sandboxes: make(map[string]*k8s.SandboxStatusInfo), + exposedPorts: make(map[string]map[int]bool), labeledSandboxes: make(map[string]string), readyCallbacks: make(map[string][]k8s.SandboxReadyCallback), + createSandboxEnv: make(map[string]map[string]string), + pvcExists: make(map[string]bool), + restoreSnapshot: make([]string, 0), } } @@ -41,6 +61,11 @@ func (m *mockRuntime) CreateSandbox(ctx context.Context, sessionID string, env m m.mu.Lock() defer m.mu.Unlock() m.createdSandboxes = append(m.createdSandboxes, sessionID) + envCopy := make(map[string]string, len(env)) + for k, v := range env { + envCopy[k] = v + } + m.createSandboxEnv[sessionID] = envCopy m.sandboxes[sessionID] = &k8s.SandboxStatusInfo{Exists: true, Ready: true, ServiceFQDN: "test.local"} return nil } @@ -130,9 +155,36 @@ func (m *mockRuntime) ListTailscaleServices(ctx context.Context) ([]string, erro } func (m *mockRuntime) ExposePort(ctx context.Context, sessionID string, port int) error { + m.mu.Lock() + defer m.mu.Unlock() + ports, exists := m.exposedPorts[sessionID] + if !exists { + ports = make(map[int]bool) + m.exposedPorts[sessionID] = ports + } + ports[port] = true + return nil +} + +func (m *mockRuntime) UnexposePort(ctx context.Context, sessionID string, port int) error { + m.mu.Lock() + defer m.mu.Unlock() + if m.unexposePortErr != nil { + return m.unexposePortErr + } + if ports, exists := m.exposedPorts[sessionID]; exists { + delete(ports, port) + } return nil } +func (m *mockRuntime) GetSandboxPreviewHostname(ctx context.Context, sessionID string) (string, error) { + if m.previewHostname != "" { + return m.previewHostname, m.previewHostErr + } + return "sandbox-" + sessionID, m.previewHostErr +} + func (m *mockRuntime) CreateOrLabelPoolSandbox(ctx context.Context, sessionID string, fromPool bool, env map[string]string) error { m.mu.Lock() defer m.mu.Unlock() @@ -200,7 +252,20 @@ func (m *mockRuntime) GetPVCName(ctx context.Context, sessionID string) (string, return "", nil } +func (m *mockRuntime) PVCExists(ctx context.Context, pvcName string) (bool, error) { + m.mu.Lock() + defer m.mu.Unlock() + exists, ok := m.pvcExists[pvcName] + if !ok { + return true, nil + } + return exists, nil +} + func (m *mockRuntime) CreatePVCFromSnapshot(ctx context.Context, sessionID, snapshotID string) (string, error) { + m.mu.Lock() + defer m.mu.Unlock() + m.restoreSnapshot = append(m.restoreSnapshot, snapshotID) return "agent-home-sess-" + sessionID, nil } @@ -247,13 +312,22 @@ func (m *mockRuntime) Close() { // mockStorage implements storage.Storage for testing type mockStorage struct { - mu sync.Mutex - sessions map[string]*pb.Session + mu sync.Mutex + sessions map[string]*pb.Session + streams map[string][]storage.StreamEntryWithID + oauth map[string]*storage.CodexOAuthSessionData + oauthGlobal *storage.CodexOAuthSessionData + pvcNames map[string]string + snapshots map[string][]*pb.Snapshot } func newMockStorage() *mockStorage { return &mockStorage{ - sessions: make(map[string]*pb.Session), + sessions: make(map[string]*pb.Session), + streams: make(map[string][]storage.StreamEntryWithID), + oauth: make(map[string]*storage.CodexOAuthSessionData), + pvcNames: make(map[string]string), + snapshots: make(map[string][]*pb.Snapshot), } } @@ -296,31 +370,166 @@ func (m *mockStorage) UpdateSessionField(ctx context.Context, id, field, value s return nil } +func (m *mockStorage) SaveSessionCodexOAuth(ctx context.Context, sessionID string, data *storage.CodexOAuthSessionData) error { + m.mu.Lock() + defer m.mu.Unlock() + if data == nil { + delete(m.oauth, sessionID) + return nil + } + c := *data + m.oauth[sessionID] = &c + return nil +} + +func (m *mockStorage) GetSessionCodexOAuth(ctx context.Context, sessionID string) (*storage.CodexOAuthSessionData, error) { + m.mu.Lock() + defer m.mu.Unlock() + data := m.oauth[sessionID] + if data == nil { + return nil, nil + } + c := *data + return &c, nil +} + +func (m *mockStorage) DeleteSessionCodexOAuth(ctx context.Context, sessionID string) error { + m.mu.Lock() + defer m.mu.Unlock() + delete(m.oauth, sessionID) + return nil +} + +func (m *mockStorage) SaveCodexOAuth(ctx context.Context, data *storage.CodexOAuthSessionData) error { + m.mu.Lock() + defer m.mu.Unlock() + if data == nil { + m.oauthGlobal = nil + return nil + } + c := *data + m.oauthGlobal = &c + return nil +} + +func (m *mockStorage) GetCodexOAuth(ctx context.Context) (*storage.CodexOAuthSessionData, error) { + m.mu.Lock() + defer m.mu.Unlock() + if m.oauthGlobal == nil { + return nil, nil + } + c := *m.oauthGlobal + return &c, nil +} + +func (m *mockStorage) DeleteCodexOAuth(ctx context.Context) error { + m.mu.Lock() + defer m.mu.Unlock() + m.oauthGlobal = nil + return nil +} + func (m *mockStorage) DeleteSession(ctx context.Context, id string) error { m.mu.Lock() defer m.mu.Unlock() delete(m.sessions, id) + delete(m.oauth, id) return nil } // Unified Stream methods (replaces messages, events, and notifications) func (m *mockStorage) AppendStreamEntry(ctx context.Context, sessionID string, entry *storage.StreamEntry) (string, error) { - return "0-0", nil + m.mu.Lock() + defer m.mu.Unlock() + + stream := m.streams[sessionID] + id := strconv.Itoa(len(stream)+1) + "-0" + entryCopy := *entry + m.streams[sessionID] = append(stream, storage.StreamEntryWithID{ + ID: id, + Entry: &entryCopy, + }) + return id, nil } func (m *mockStorage) GetStreamEntries(ctx context.Context, sessionID string, afterID string, limit int) ([]storage.StreamEntryWithID, error) { - return nil, nil + m.mu.Lock() + defer m.mu.Unlock() + return m.filterStreamEntriesLocked(sessionID, afterID, limit, nil), nil } func (m *mockStorage) GetStreamEntriesByTypes(ctx context.Context, sessionID string, afterID string, limit int, types []string) ([]storage.StreamEntryWithID, error) { - return nil, nil + m.mu.Lock() + defer m.mu.Unlock() + return m.filterStreamEntriesLocked(sessionID, afterID, limit, types), nil } func (m *mockStorage) GetLastStreamID(ctx context.Context, sessionID string) (string, error) { - return "0-0", nil + m.mu.Lock() + defer m.mu.Unlock() + stream := m.streams[sessionID] + if len(stream) == 0 { + return "0-0", nil + } + return stream[len(stream)-1].ID, nil +} + +func (m *mockStorage) filterStreamEntriesLocked(sessionID, afterID string, limit int, types []string) []storage.StreamEntryWithID { + stream := m.streams[sessionID] + if len(stream) == 0 { + return nil + } + + typeFilter := map[string]bool{} + for _, t := range types { + typeFilter[t] = true + } + + afterSeq := 0 + if afterID != "" && afterID != "0" { + afterSeq = parseStreamSeq(afterID) + } + + result := make([]storage.StreamEntryWithID, 0, len(stream)) + for _, e := range stream { + if parseStreamSeq(e.ID) <= afterSeq { + continue + } + if len(typeFilter) > 0 && !typeFilter[e.Entry.Type] { + continue + } + result = append(result, e) + if limit > 0 && len(result) >= limit { + break + } + } + return result +} + +func parseStreamSeq(id string) int { + parts := strings.SplitN(id, "-", 2) + if len(parts) == 0 { + return 0 + } + n, err := strconv.Atoi(parts[0]) + if err != nil { + return 0 + } + return n } func (m *mockStorage) TruncateStreamAfter(ctx context.Context, sessionID string, afterID string) error { + m.mu.Lock() + defer m.mu.Unlock() + afterSeq := parseStreamSeq(afterID) + stream := m.streams[sessionID] + filtered := stream[:0] + for _, e := range stream { + if parseStreamSeq(e.ID) <= afterSeq { + filtered = append(filtered, e) + } + } + m.streams[sessionID] = filtered return nil } @@ -346,22 +555,50 @@ func (m *mockStorage) Close() error { // Snapshot methods func (m *mockStorage) SaveSnapshot(ctx context.Context, s *pb.Snapshot) error { + m.mu.Lock() + defer m.mu.Unlock() + m.snapshots[s.SessionId] = append([]*pb.Snapshot{s}, m.snapshots[s.SessionId]...) return nil } func (m *mockStorage) GetSnapshot(ctx context.Context, sessionID, snapshotID string) (*pb.Snapshot, error) { + m.mu.Lock() + defer m.mu.Unlock() + for _, s := range m.snapshots[sessionID] { + if s != nil && s.Id == snapshotID { + return s, nil + } + } return nil, nil } func (m *mockStorage) ListSnapshots(ctx context.Context, sessionID string) ([]*pb.Snapshot, error) { - return nil, nil + m.mu.Lock() + defer m.mu.Unlock() + snaps := m.snapshots[sessionID] + out := make([]*pb.Snapshot, len(snaps)) + copy(out, snaps) + return out, nil } func (m *mockStorage) DeleteSnapshot(ctx context.Context, sessionID, snapshotID string) error { + m.mu.Lock() + defer m.mu.Unlock() + snaps := m.snapshots[sessionID] + filtered := make([]*pb.Snapshot, 0, len(snaps)) + for _, s := range snaps { + if s == nil || s.Id != snapshotID { + filtered = append(filtered, s) + } + } + m.snapshots[sessionID] = filtered return nil } func (m *mockStorage) DeleteAllSnapshots(ctx context.Context, sessionID string) error { + m.mu.Lock() + defer m.mu.Unlock() + delete(m.snapshots, sessionID) return nil } @@ -390,11 +627,16 @@ func (m *mockStorage) ClearOldPVCName(ctx context.Context, sessionID string) err } func (m *mockStorage) SetPVCName(ctx context.Context, sessionID, pvcName string) error { + m.mu.Lock() + defer m.mu.Unlock() + m.pvcNames[sessionID] = pvcName return nil } func (m *mockStorage) GetPVCName(ctx context.Context, sessionID string) (string, error) { - return "", nil + m.mu.Lock() + defer m.mu.Unlock() + return m.pvcNames[sessionID], nil } // Helper to create a test manager with mock dependencies @@ -422,6 +664,126 @@ func addSession(m *Manager, id string, status pb.SessionStatus, lastActiveAt tim m.sessions[id] = NewSessionState(session) } +func TestCreateSandboxDirect_UsesStoredPVCWhenItExists(t *testing.T) { + manager, runtime, store := newTestManager(3) + sessionID := "sess-existing-pvc" + addSession(manager, sessionID, pb.SessionStatus_SESSION_STATUS_PAUSED, time.Now()) + + storedPVC := "agent-home-netclode-agent-pool-abc123" + if err := store.SetPVCName(context.Background(), sessionID, storedPVC); err != nil { + t.Fatalf("SetPVCName failed: %v", err) + } + + runtime.mu.Lock() + runtime.pvcExists[storedPVC] = true + runtime.mu.Unlock() + + manager.createSandboxDirect(context.Background(), sessionID, nil, nil, false, nil) + + runtime.mu.Lock() + env := runtime.createSandboxEnv[sessionID] + runtime.mu.Unlock() + + if got := env[k8s.ExistingPVCEnvKey]; got != storedPVC { + t.Fatalf("expected %s=%q, got %q", k8s.ExistingPVCEnvKey, storedPVC, got) + } +} + +func TestCreateSandboxDirect_SkipsStoredPVCWhenMissingAndNoSnapshots(t *testing.T) { + manager, runtime, store := newTestManager(3) + sessionID := "sess-missing-pvc" + addSession(manager, sessionID, pb.SessionStatus_SESSION_STATUS_PAUSED, time.Now()) + + storedPVC := "agent-home-sess-does-not-exist" + if err := store.SetPVCName(context.Background(), sessionID, storedPVC); err != nil { + t.Fatalf("SetPVCName failed: %v", err) + } + + runtime.mu.Lock() + runtime.pvcExists[storedPVC] = false + runtime.pvcExists["agent-home-sess-"+sessionID] = false + runtime.mu.Unlock() + + manager.createSandboxDirect(context.Background(), sessionID, nil, nil, false, nil) + + runtime.mu.Lock() + env := runtime.createSandboxEnv[sessionID] + runtime.mu.Unlock() + + if _, ok := env[k8s.ExistingPVCEnvKey]; ok { + t.Fatalf("did not expect %s to be set when stored PVC is missing", k8s.ExistingPVCEnvKey) + } +} + +func TestCreateSandboxDirect_UsesCanonicalPVCWhenStoredPVCMissing(t *testing.T) { + manager, runtime, store := newTestManager(3) + sessionID := "sess-missing-pvc-canonical-exists" + addSession(manager, sessionID, pb.SessionStatus_SESSION_STATUS_PAUSED, time.Now()) + + storedPVC := "agent-home-netclode-agent-pool-missing" + if err := store.SetPVCName(context.Background(), sessionID, storedPVC); err != nil { + t.Fatalf("SetPVCName failed: %v", err) + } + + canonicalPVC := "agent-home-sess-" + sessionID + runtime.mu.Lock() + runtime.pvcExists[storedPVC] = false + runtime.pvcExists[canonicalPVC] = true + runtime.mu.Unlock() + + manager.createSandboxDirect(context.Background(), sessionID, nil, nil, false, nil) + + runtime.mu.Lock() + env := runtime.createSandboxEnv[sessionID] + restoreCalls := append([]string(nil), runtime.restoreSnapshot...) + runtime.mu.Unlock() + + if got := env[k8s.ExistingPVCEnvKey]; got != canonicalPVC { + t.Fatalf("expected %s=%q, got %q", k8s.ExistingPVCEnvKey, canonicalPVC, got) + } + + if len(restoreCalls) != 0 { + t.Fatalf("expected no snapshot restore calls when canonical PVC exists, got %v", restoreCalls) + } +} + +func TestCreateSandboxDirect_RestoresLatestSnapshotWhenStoredPVCMissing(t *testing.T) { + manager, runtime, store := newTestManager(3) + sessionID := "sess-missing-pvc-with-snapshots" + addSession(manager, sessionID, pb.SessionStatus_SESSION_STATUS_PAUSED, time.Now()) + + storedPVC := "agent-home-sess-does-not-exist" + if err := store.SetPVCName(context.Background(), sessionID, storedPVC); err != nil { + t.Fatalf("SetPVCName failed: %v", err) + } + + store.snapshots[sessionID] = []*pb.Snapshot{ + {Id: "latest-snapshot", SessionId: sessionID}, + {Id: "older-snapshot", SessionId: sessionID}, + } + + runtime.mu.Lock() + runtime.pvcExists[storedPVC] = false + runtime.pvcExists["agent-home-sess-"+sessionID] = false + runtime.mu.Unlock() + + manager.createSandboxDirect(context.Background(), sessionID, nil, nil, false, nil) + + runtime.mu.Lock() + env := runtime.createSandboxEnv[sessionID] + restoreCalls := append([]string(nil), runtime.restoreSnapshot...) + runtime.mu.Unlock() + + expectedPVC := "agent-home-sess-" + sessionID + if got := env[k8s.ExistingPVCEnvKey]; got != expectedPVC { + t.Fatalf("expected %s=%q, got %q", k8s.ExistingPVCEnvKey, expectedPVC, got) + } + + if len(restoreCalls) != 1 || restoreCalls[0] != "latest-snapshot" { + t.Fatalf("expected restore call with latest snapshot, got %v", restoreCalls) + } +} + func TestEnsureActiveSlot_NoActionWhenUnderLimit(t *testing.T) { manager, runtime, _ := newTestManager(3) @@ -586,10 +948,20 @@ type mockWarmAgentConnection struct { assignedConfig *AgentSessionConfig assignCalled bool assignError error + executeCalled bool + executeText string + executeErr error + codexAuthCalled bool + codexAccessToken string + codexIdToken string + codexExpiresAt *timestamppb.Timestamp + codexAuthErr error } func (m *mockWarmAgentConnection) ExecutePrompt(text string) error { - return nil + m.executeCalled = true + m.executeText = text + return m.executeErr } func (m *mockWarmAgentConnection) Interrupt() error { @@ -620,6 +992,14 @@ func (m *mockWarmAgentConnection) UpdateGitCredentials(token string, repoAccess return nil } +func (m *mockWarmAgentConnection) UpdateCodexAuth(accessToken, idToken string, expiresAt *timestamppb.Timestamp) error { + m.codexAuthCalled = true + m.codexAccessToken = accessToken + m.codexIdToken = idToken + m.codexExpiresAt = expiresAt + return m.codexAuthErr +} + func (m *mockWarmAgentConnection) AssignSession(sessionID string, config *AgentSessionConfig) error { m.assignCalled = true m.assignedSessionID = sessionID @@ -770,3 +1150,384 @@ func TestMultipleWarmAgents(t *testing.T) { t.Error("sess-b should be assigned to conn2") } } + +func TestCreate_CodexOAuthRequiresGlobalCredential(t *testing.T) { + manager, _, store := newTestManager(3) + manager.config.UseWarmPool = false + + sdkType := pb.SdkType_SDK_TYPE_CODEX + model := "gpt-5-codex:oauth:high" + _, err := manager.Create(context.Background(), "OAuth Session", nil, nil, &sdkType, &model, nil, nil, nil) + if err == nil { + t.Fatal("expected Create to fail when global codex oauth is not configured") + } + + now := time.Now().UTC() + expiresAt := now.Add(30 * time.Minute) + if err := store.SaveCodexOAuth(context.Background(), &storage.CodexOAuthSessionData{ + AccessToken: "access-token", + IdToken: "id-token", + RefreshToken: "refresh-token", + ExpiresAt: &expiresAt, + UpdatedAt: now, + }); err != nil { + t.Fatalf("SaveCodexOAuth failed: %v", err) + } + + sess, err := manager.Create(context.Background(), "OAuth Session", nil, nil, &sdkType, &model, nil, nil, nil) + if err != nil { + t.Fatalf("Create failed: %v", err) + } + if sess == nil || sess.Id == "" { + t.Fatalf("unexpected session returned: %+v", sess) + } +} + +func TestGetSessionConfig_CodexOAuthOmitsRefreshToken(t *testing.T) { + manager, _, store := newTestManager(3) + manager.config.OpenAIAPIKey = "should-not-be-sent-in-oauth-mode" + sessionID := "sess-oauth-config" + now := time.Now().UTC() + model := "gpt-5-codex:oauth:high" + sdkType := pb.SdkType_SDK_TYPE_CODEX + + session := &pb.Session{ + Id: sessionID, + Name: "OAuth Session", + Status: pb.SessionStatus_SESSION_STATUS_READY, + CreatedAt: timestamppb.New(now), + LastActiveAt: timestamppb.New(now), + SdkType: &sdkType, + Model: &model, + } + manager.sessions[sessionID] = NewSessionState(session) + if err := store.SaveSession(context.Background(), session); err != nil { + t.Fatalf("SaveSession failed: %v", err) + } + expiresAt := now.Add(20 * time.Minute) + if err := store.SaveCodexOAuth(context.Background(), &storage.CodexOAuthSessionData{ + AccessToken: "oauth-access", + IdToken: "oauth-id", + RefreshToken: "oauth-refresh", + ExpiresAt: &expiresAt, + UpdatedAt: now, + }); err != nil { + t.Fatalf("SaveCodexOAuth failed: %v", err) + } + + cfg, err := manager.GetSessionConfig(context.Background(), sessionID) + if err != nil { + t.Fatalf("GetSessionConfig failed: %v", err) + } + if cfg.CodexAccessToken != "oauth-access" || cfg.CodexIdToken != "oauth-id" { + t.Fatalf("unexpected codex oauth tokens in config: %+v", cfg) + } + if cfg.OpenAIAPIKey != "" { + t.Fatalf("expected OpenAI API key to be omitted in oauth mode, got %q", cfg.OpenAIAPIKey) + } +} + +func TestGetSessionConfig_CodexModelMissingAuthSuffixFails(t *testing.T) { + manager, _, store := newTestManager(3) + sessionID := "sess-codex-no-auth-suffix" + now := time.Now().UTC() + model := "gpt-5-codex" + sdkType := pb.SdkType_SDK_TYPE_CODEX + + session := &pb.Session{ + Id: sessionID, + Name: "Codex Session", + Status: pb.SessionStatus_SESSION_STATUS_READY, + CreatedAt: timestamppb.New(now), + LastActiveAt: timestamppb.New(now), + SdkType: &sdkType, + Model: &model, + } + manager.sessions[sessionID] = NewSessionState(session) + if err := store.SaveSession(context.Background(), session); err != nil { + t.Fatalf("SaveSession failed: %v", err) + } + + _, err := manager.GetSessionConfig(context.Background(), sessionID) + if err == nil { + t.Fatal("expected GetSessionConfig to fail for codex model without auth suffix") + } + if !strings.Contains(err.Error(), "missing auth suffix") { + t.Fatalf("unexpected error: %v", err) + } +} + +func TestSendPrompt_CodexOAuthRefreshesAndPushesUpdatedTokens(t *testing.T) { + manager, _, store := newTestManager(3) + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`{"access_token":"new-access","id_token":"new-id","refresh_token":"new-refresh","expires_in":3600}`)) + })) + defer server.Close() + + t.Setenv("CODEX_REFRESH_TOKEN_URL_OVERRIDE", server.URL) + + // Ensure mock storage can persist oauth data with keyless mock implementation. + manager.config.CodexOAuthEncryptionKey = []byte("0123456789abcdef0123456789abcdef") + + sessionID := "sess-sendprompt-oauth" + now := time.Now().UTC() + sdkType := pb.SdkType_SDK_TYPE_CODEX + model := "gpt-5-codex:oauth:high" + session := &pb.Session{ + Id: sessionID, + Name: "OAuth Prompt", + Status: pb.SessionStatus_SESSION_STATUS_READY, + CreatedAt: timestamppb.New(now), + LastActiveAt: timestamppb.New(now), + SdkType: &sdkType, + Model: &model, + } + state := NewSessionState(session) + manager.sessions[sessionID] = state + _ = store.SaveSession(context.Background(), session) + + expiredAt := now.Add(1 * time.Minute) + _ = store.SaveCodexOAuth(context.Background(), &storage.CodexOAuthSessionData{ + AccessToken: "old-access", + IdToken: "old-id", + RefreshToken: "old-refresh", + ExpiresAt: &expiredAt, + UpdatedAt: now, + }) + + conn := &mockWarmAgentConnection{} + manager.RegisterAgentConnection(sessionID, conn) + defer manager.UnregisterAgentConnection(sessionID) + + if err := manager.SendPrompt(context.Background(), sessionID, "hello"); err != nil { + t.Fatalf("SendPrompt failed: %v", err) + } + + if !conn.codexAuthCalled { + t.Fatal("expected UpdateCodexAuth to be called") + } + if conn.codexAccessToken != "new-access" || conn.codexIdToken != "new-id" { + t.Fatalf("unexpected updated codex tokens: access=%q id=%q", conn.codexAccessToken, conn.codexIdToken) + } + + updated, err := store.GetCodexOAuth(context.Background()) + if err != nil { + t.Fatalf("GetCodexOAuth failed: %v", err) + } + if updated == nil || updated.RefreshToken != "new-refresh" { + b, _ := json.Marshal(updated) + t.Fatalf("expected refreshed oauth data in storage, got %s", b) + } + if !conn.executeCalled { + t.Fatal("expected ExecutePrompt to be called") + } +} + +func TestStartCodexAuth_RequiresEncryptionKey(t *testing.T) { + manager, _, _ := newTestManager(3) + manager.config.CodexOAuthEncryptionKey = nil + + _, err := manager.StartCodexAuth(context.Background()) + if err == nil { + t.Fatal("expected StartCodexAuth to fail without encryption key") + } + if !strings.Contains(err.Error(), "CODEX_OAUTH_ENCRYPTION_KEY_B64") { + t.Fatalf("unexpected error: %v", err) + } +} + +func TestGetAllowedSecretForHost_CodexHasNoProxySecret(t *testing.T) { + manager, _, _ := newTestManager(3) + + secretKey, placeholder := manager.getAllowedSecretForHost(pb.SdkType_SDK_TYPE_CODEX, "api.openai.com") + if secretKey != "" || placeholder != "" { + t.Fatalf("expected no proxy secret mapping for codex, got secretKey=%q placeholder=%q", secretKey, placeholder) + } +} + +func TestHandleAgentResponse_SystemInterruptedSetsReady(t *testing.T) { + manager, _, _ := newTestManager(3) + now := time.Now().UTC() + addSession(manager, "sess-interrupt", pb.SessionStatus_SESSION_STATUS_RUNNING, now) + + state := manager.sessions["sess-interrupt"] + state.CurrentMessageID = "msg_123" + state.ContentBuilder.WriteString("partial content") + state.OriginalPrompt = "long running prompt" + + err := manager.HandleAgentResponse(context.Background(), "sess-interrupt", &pb.AgentStreamResponse{ + Response: &pb.AgentStreamResponse_SystemMessage{ + SystemMessage: &pb.AgentSystemMessage{Message: "interrupted"}, + }, + }) + if err != nil { + t.Fatalf("HandleAgentResponse failed: %v", err) + } + + if got := manager.sessions["sess-interrupt"].Session.Status; got != pb.SessionStatus_SESSION_STATUS_READY { + t.Fatalf("expected status READY after interrupt, got %s", got) + } + if got := state.CurrentMessageID; got != "" { + t.Fatalf("expected CurrentMessageID to be reset, got %q", got) + } + if got := state.ContentBuilder.String(); got != "" { + t.Fatalf("expected ContentBuilder to be reset, got %q", got) + } + if got := state.OriginalPrompt; got != "" { + t.Fatalf("expected OriginalPrompt to be reset, got %q", got) + } +} + +func TestHandleAgentResponse_SystemMessageNonInterruptNoStatusChange(t *testing.T) { + manager, _, _ := newTestManager(3) + now := time.Now().UTC() + addSession(manager, "sess-system", pb.SessionStatus_SESSION_STATUS_RUNNING, now) + + err := manager.HandleAgentResponse(context.Background(), "sess-system", &pb.AgentStreamResponse{ + Response: &pb.AgentStreamResponse_SystemMessage{ + SystemMessage: &pb.AgentSystemMessage{Message: "ready"}, + }, + }) + if err != nil { + t.Fatalf("HandleAgentResponse failed: %v", err) + } + + if got := manager.sessions["sess-system"].Session.Status; got != pb.SessionStatus_SESSION_STATUS_RUNNING { + t.Fatalf("expected status to stay RUNNING for non-interrupt system message, got %s", got) + } +} + +func TestExposePort_UsesResolvedPreviewHostname(t *testing.T) { + manager, runtime, _ := newTestManager(3) + runtime.previewHostname = "sandbox-test123.dolly-grouse.ts.net" + + previewURL, err := manager.ExposePort(context.Background(), "test123", 1234) + if err != nil { + t.Fatalf("ExposePort failed: %v", err) + } + + expected := "http://sandbox-test123.dolly-grouse.ts.net:1234" + if previewURL != expected { + t.Fatalf("expected preview URL %q, got %q", expected, previewURL) + } +} + +func TestExposePort_FallsBackToShortHostnameOnLookupError(t *testing.T) { + manager, runtime, _ := newTestManager(3) + runtime.previewHostErr = errors.New("lookup failed") + + previewURL, err := manager.ExposePort(context.Background(), "test123", 1234) + if err != nil { + t.Fatalf("ExposePort failed: %v", err) + } + + expected := "http://sandbox-test123:1234" + if previewURL != expected { + t.Fatalf("expected preview URL %q, got %q", expected, previewURL) + } +} + +func TestUnexposePort_RemovesPortAndPersistsEvent(t *testing.T) { + manager, runtime, _ := newTestManager(3) + + if _, err := manager.ExposePort(context.Background(), "test123", 1234); err != nil { + t.Fatalf("ExposePort failed: %v", err) + } + if err := manager.UnexposePort(context.Background(), "test123", 1234); err != nil { + t.Fatalf("UnexposePort failed: %v", err) + } + + runtime.mu.Lock() + _, stillExposed := runtime.exposedPorts["test123"][1234] + runtime.mu.Unlock() + if stillExposed { + t.Fatalf("expected port 1234 to be removed from runtime state") + } + + entries, err := manager.storage.GetStreamEntriesByTypes(context.Background(), "test123", "0", 0, []string{storage.StreamEntryTypeEvent}) + if err != nil { + t.Fatalf("GetStreamEntriesByTypes failed: %v", err) + } + + foundUnexposed := false + for _, e := range entries { + var event pb.AgentEvent + if err := protojson.Unmarshal(e.Entry.Payload, &event); err != nil { + continue + } + if event.Kind == pb.AgentEventKind_AGENT_EVENT_KIND_PORT_UNEXPOSED { + if payload := event.GetPortUnexposed(); payload != nil && payload.Port == 1234 { + foundUnexposed = true + break + } + } + } + + if !foundUnexposed { + t.Fatalf("expected persisted port_unexposed event for port 1234") + } +} + +func TestUnexposePort_NotFoundStillPersistsEvent(t *testing.T) { + manager, runtime, _ := newTestManager(3) + runtime.unexposePortErr = k8serrors.NewNotFound(schema.GroupResource{Resource: "services"}, "ts-test123") + + if err := manager.UnexposePort(context.Background(), "test123", 1234); err != nil { + t.Fatalf("UnexposePort failed: %v", err) + } + + entries, err := manager.storage.GetStreamEntriesByTypes(context.Background(), "test123", "0", 0, []string{storage.StreamEntryTypeEvent}) + if err != nil { + t.Fatalf("GetStreamEntriesByTypes failed: %v", err) + } + + foundUnexposed := false + for _, e := range entries { + var event pb.AgentEvent + if err := protojson.Unmarshal(e.Entry.Payload, &event); err != nil { + continue + } + if event.Kind == pb.AgentEventKind_AGENT_EVENT_KIND_PORT_UNEXPOSED { + if payload := event.GetPortUnexposed(); payload != nil && payload.Port == 1234 { + foundUnexposed = true + break + } + } + } + + if !foundUnexposed { + t.Fatalf("expected persisted port_unexposed event for port 1234") + } +} + +func TestRestoreExposedPorts_AppliesLatestExposeState(t *testing.T) { + manager, runtime, _ := newTestManager(3) + + if _, err := manager.ExposePort(context.Background(), "test123", 1234); err != nil { + t.Fatalf("ExposePort(1234) failed: %v", err) + } + if _, err := manager.ExposePort(context.Background(), "test123", 5678); err != nil { + t.Fatalf("ExposePort(5678) failed: %v", err) + } + if err := manager.UnexposePort(context.Background(), "test123", 1234); err != nil { + t.Fatalf("UnexposePort(1234) failed: %v", err) + } + + // Simulate fresh runtime state before resume restoration. + runtime.mu.Lock() + runtime.exposedPorts["test123"] = make(map[int]bool) + runtime.mu.Unlock() + + manager.restoreExposedPorts(context.Background(), "test123") + + runtime.mu.Lock() + defer runtime.mu.Unlock() + if !runtime.exposedPorts["test123"][5678] { + t.Fatalf("expected port 5678 to be restored") + } + if runtime.exposedPorts["test123"][1234] { + t.Fatalf("expected port 1234 to remain unexposed after restoration") + } +} diff --git a/services/control-plane/internal/session/state.go b/services/control-plane/internal/session/state.go index 3877482f..97c84fa4 100644 --- a/services/control-plane/internal/session/state.go +++ b/services/control-plane/internal/session/state.go @@ -5,6 +5,7 @@ import ( "time" pb "github.com/angristan/netclode/services/control-plane/gen/netclode/v1" + "github.com/angristan/netclode/services/control-plane/internal/storage" ) // SessionState holds the in-memory state for a session. @@ -26,6 +27,9 @@ type SessionState struct { // Restore state - when set, next sandbox creation restores from this snapshot RestoreSnapshotID string + + // Session-scoped Codex OAuth tokens (persisted encrypted in Redis). + CodexOAuth *storage.CodexOAuthSessionData } // NewSessionState creates a new session state. diff --git a/services/control-plane/internal/storage/redis.go b/services/control-plane/internal/storage/redis.go index d4d862ab..c790ece1 100644 --- a/services/control-plane/internal/storage/redis.go +++ b/services/control-plane/internal/storage/redis.go @@ -2,6 +2,10 @@ package storage import ( "context" + "crypto/aes" + "crypto/cipher" + "crypto/rand" + "encoding/base64" "encoding/json" "fmt" "log/slog" @@ -15,7 +19,10 @@ import ( ) const ( - keySessionsAll = "sessions:all" + keySessionsAll = "sessions:all" + keyCodexOAuthGlobal = "codex:oauth" + fieldCodexOAuth = "codexOAuthEncV1" + fieldCodexOAuthGlobal = "oauthEncV1" ) func snapshotsSetKey(sessionID string) string { @@ -107,6 +114,9 @@ func (r *RedisStorage) SaveSession(ctx context.Context, s *pb.Session) error { if s.Model != nil { pipe.HSet(ctx, sessionKey(s.Id), "model", *s.Model) } + if s.CopilotBackend != nil { + pipe.HSet(ctx, sessionKey(s.Id), "copilotBackend", s.CopilotBackend.String()) + } _, err := pipe.Exec(ctx) return err @@ -154,6 +164,12 @@ func (r *RedisStorage) GetSession(ctx context.Context, id string) (*pb.Session, if model, ok := data["model"]; ok && model != "" { session.Model = &model } + if backendStr, ok := data["copilotBackend"]; ok && backendStr != "" { + backend := parseCopilotBackend(backendStr) + if backend != pb.CopilotBackend_COPILOT_BACKEND_UNSPECIFIED { + session.CopilotBackend = &backend + } + } return session, nil } @@ -187,11 +203,26 @@ func parseSdkType(s string) pb.SdkType { return pb.SdkType_SDK_TYPE_CLAUDE case "SDK_TYPE_OPENCODE": return pb.SdkType_SDK_TYPE_OPENCODE + case "SDK_TYPE_COPILOT": + return pb.SdkType_SDK_TYPE_COPILOT + case "SDK_TYPE_CODEX": + return pb.SdkType_SDK_TYPE_CODEX default: return pb.SdkType_SDK_TYPE_UNSPECIFIED } } +func parseCopilotBackend(s string) pb.CopilotBackend { + switch s { + case "COPILOT_BACKEND_GITHUB": + return pb.CopilotBackend_COPILOT_BACKEND_GITHUB + case "COPILOT_BACKEND_ANTHROPIC": + return pb.CopilotBackend_COPILOT_BACKEND_ANTHROPIC + default: + return pb.CopilotBackend_COPILOT_BACKEND_UNSPECIFIED + } +} + func parseRepoAccess(s string) pb.RepoAccess { switch strings.ToLower(s) { case "repo_access_read", "read": @@ -240,24 +271,37 @@ func (r *RedisStorage) GetAllSessions(ctx context.Context) ([]*pb.Session, error if t, err := time.Parse(time.RFC3339, data["createdAt"]); err == nil { session.CreatedAt = timestamppb.New(t) } - if t, err := time.Parse(time.RFC3339, data["lastActiveAt"]); err == nil { - session.LastActiveAt = timestamppb.New(t) - } - if reposJSON, ok := data["repos"]; ok && reposJSON != "" { - var repos []string - if err := json.Unmarshal([]byte(reposJSON), &repos); err == nil { - session.Repos = repos - } else { - slog.Warn("Failed to decode repos for session", "sessionID", id, "error", err) + if t, err := time.Parse(time.RFC3339, data["lastActiveAt"]); err == nil { + session.LastActiveAt = timestamppb.New(t) } - } - if repoAccessStr, ok := data["repoAccess"]; ok && repoAccessStr != "" { - repoAccess := parseRepoAccess(repoAccessStr) - if repoAccess != pb.RepoAccess_REPO_ACCESS_UNSPECIFIED { - session.RepoAccess = &repoAccess + if reposJSON, ok := data["repos"]; ok && reposJSON != "" { + var repos []string + if err := json.Unmarshal([]byte(reposJSON), &repos); err == nil { + session.Repos = repos + } else { + slog.Warn("Failed to decode repos for session", "sessionID", id, "error", err) + } } - } - sessions = append(sessions, session) + if repoAccessStr, ok := data["repoAccess"]; ok && repoAccessStr != "" { + repoAccess := parseRepoAccess(repoAccessStr) + if repoAccess != pb.RepoAccess_REPO_ACCESS_UNSPECIFIED { + session.RepoAccess = &repoAccess + } + } + if sdkTypeStr, ok := data["sdkType"]; ok && sdkTypeStr != "" { + sdkType := parseSdkType(sdkTypeStr) + session.SdkType = &sdkType + } + if model, ok := data["model"]; ok && model != "" { + session.Model = &model + } + if backendStr, ok := data["copilotBackend"]; ok && backendStr != "" { + backend := parseCopilotBackend(backendStr) + if backend != pb.CopilotBackend_COPILOT_BACKEND_UNSPECIFIED { + session.CopilotBackend = &backend + } + } + sessions = append(sessions, session) } return sessions, nil @@ -273,6 +317,148 @@ func (r *RedisStorage) UpdateSessionField(ctx context.Context, id, field, value return r.client.HSet(ctx, sessionKey(id), field, value).Err() } +// SaveSessionCodexOAuth stores encrypted Codex OAuth tokens for a session. +func (r *RedisStorage) SaveSessionCodexOAuth(ctx context.Context, sessionID string, data *CodexOAuthSessionData) error { + if data == nil { + return fmt.Errorf("codex oauth data is required") + } + if data.UpdatedAt.IsZero() { + data.UpdatedAt = time.Now().UTC() + } + + raw, err := json.Marshal(data) + if err != nil { + return fmt.Errorf("marshal codex oauth data: %w", err) + } + encrypted, err := encryptWithKey(raw, r.config.CodexOAuthEncryptionKey) + if err != nil { + return fmt.Errorf("encrypt codex oauth data: %w", err) + } + + return r.client.HSet(ctx, sessionKey(sessionID), fieldCodexOAuth, encrypted).Err() +} + +// GetSessionCodexOAuth retrieves and decrypts Codex OAuth tokens for a session. +func (r *RedisStorage) GetSessionCodexOAuth(ctx context.Context, sessionID string) (*CodexOAuthSessionData, error) { + encrypted, err := r.client.HGet(ctx, sessionKey(sessionID), fieldCodexOAuth).Result() + if err == redis.Nil { + return nil, nil + } + if err != nil { + return nil, err + } + + raw, err := decryptWithKey(encrypted, r.config.CodexOAuthEncryptionKey) + if err != nil { + return nil, fmt.Errorf("decrypt codex oauth data: %w", err) + } + + var data CodexOAuthSessionData + if err := json.Unmarshal(raw, &data); err != nil { + return nil, fmt.Errorf("unmarshal codex oauth data: %w", err) + } + return &data, nil +} + +// DeleteSessionCodexOAuth removes stored Codex OAuth tokens for a session. +func (r *RedisStorage) DeleteSessionCodexOAuth(ctx context.Context, sessionID string) error { + return r.client.HDel(ctx, sessionKey(sessionID), fieldCodexOAuth).Err() +} + +// SaveCodexOAuth stores encrypted global Codex OAuth credentials. +func (r *RedisStorage) SaveCodexOAuth(ctx context.Context, data *CodexOAuthSessionData) error { + if data == nil { + return fmt.Errorf("codex oauth data is required") + } + if data.UpdatedAt.IsZero() { + data.UpdatedAt = time.Now().UTC() + } + + raw, err := json.Marshal(data) + if err != nil { + return fmt.Errorf("marshal codex oauth data: %w", err) + } + encrypted, err := encryptWithKey(raw, r.config.CodexOAuthEncryptionKey) + if err != nil { + return fmt.Errorf("encrypt codex oauth data: %w", err) + } + + return r.client.HSet(ctx, keyCodexOAuthGlobal, fieldCodexOAuthGlobal, encrypted).Err() +} + +// GetCodexOAuth retrieves and decrypts global Codex OAuth credentials. +func (r *RedisStorage) GetCodexOAuth(ctx context.Context) (*CodexOAuthSessionData, error) { + encrypted, err := r.client.HGet(ctx, keyCodexOAuthGlobal, fieldCodexOAuthGlobal).Result() + if err == redis.Nil { + return nil, nil + } + if err != nil { + return nil, err + } + + raw, err := decryptWithKey(encrypted, r.config.CodexOAuthEncryptionKey) + if err != nil { + return nil, fmt.Errorf("decrypt codex oauth data: %w", err) + } + + var data CodexOAuthSessionData + if err := json.Unmarshal(raw, &data); err != nil { + return nil, fmt.Errorf("unmarshal codex oauth data: %w", err) + } + return &data, nil +} + +// DeleteCodexOAuth removes global Codex OAuth credentials. +func (r *RedisStorage) DeleteCodexOAuth(ctx context.Context) error { + return r.client.HDel(ctx, keyCodexOAuthGlobal, fieldCodexOAuthGlobal).Err() +} + +func encryptWithKey(plaintext []byte, key []byte) (string, error) { + if len(key) != 32 { + return "", fmt.Errorf("CODEX_OAUTH_ENCRYPTION_KEY_B64 must decode to 32 bytes") + } + block, err := aes.NewCipher(key) + if err != nil { + return "", err + } + gcm, err := cipher.NewGCM(block) + if err != nil { + return "", err + } + nonce := make([]byte, gcm.NonceSize()) + if _, err := rand.Read(nonce); err != nil { + return "", err + } + ciphertext := gcm.Seal(nil, nonce, plaintext, nil) + packed := append(nonce, ciphertext...) + return base64.StdEncoding.EncodeToString(packed), nil +} + +func decryptWithKey(encoded string, key []byte) ([]byte, error) { + if len(key) != 32 { + return nil, fmt.Errorf("CODEX_OAUTH_ENCRYPTION_KEY_B64 must decode to 32 bytes") + } + packed, err := base64.StdEncoding.DecodeString(encoded) + if err != nil { + return nil, err + } + block, err := aes.NewCipher(key) + if err != nil { + return nil, err + } + gcm, err := cipher.NewGCM(block) + if err != nil { + return nil, err + } + nonceSize := gcm.NonceSize() + if len(packed) < nonceSize { + return nil, fmt.Errorf("invalid ciphertext") + } + nonce := packed[:nonceSize] + ciphertext := packed[nonceSize:] + return gcm.Open(nil, nonce, ciphertext, nil) +} + // SetRestoreSnapshotID stores the snapshot ID to restore from (persisted for crash recovery). func (r *RedisStorage) SetRestoreSnapshotID(ctx context.Context, sessionID, snapshotID string) error { return r.client.HSet(ctx, sessionKey(sessionID), "restoreSnapshotID", snapshotID).Err() @@ -319,6 +505,9 @@ func (r *RedisStorage) DeleteSession(ctx context.Context, id string) error { if err := r.DeleteAllSnapshots(ctx, id); err != nil { slog.Warn("failed to delete snapshots during session delete", "sessionID", id, "error", err) } + if err := r.DeleteSessionCodexOAuth(ctx, id); err != nil { + slog.Warn("failed to delete codex oauth data during session delete", "sessionID", id, "error", err) + } pipe := r.client.TxPipeline() pipe.SRem(ctx, keySessionsAll, id) diff --git a/services/control-plane/internal/storage/redis_test.go b/services/control-plane/internal/storage/redis_test.go index 4199f622..457ef0ee 100644 --- a/services/control-plane/internal/storage/redis_test.go +++ b/services/control-plane/internal/storage/redis_test.go @@ -261,6 +261,66 @@ func TestStreamKey(t *testing.T) { } } +func TestSessionCodexOAuthStorage_EncryptedRoundTrip(t *testing.T) { + mr, storage := setupTestRedis(t) + defer mr.Close() + defer storage.Close() + + storage.config.CodexOAuthEncryptionKey = []byte("0123456789abcdef0123456789abcdef") + + ctx := context.Background() + sessionID := "oauth-session-1" + expiresAt := time.Now().UTC().Add(30 * time.Minute).Truncate(time.Second) + data := &CodexOAuthSessionData{ + AccessToken: "access-token-123", + IdToken: "id-token-456", + RefreshToken: "refresh-token-789", + ExpiresAt: &expiresAt, + UpdatedAt: time.Now().UTC(), + } + + if err := storage.SaveSessionCodexOAuth(ctx, sessionID, data); err != nil { + t.Fatalf("save codex oauth: %v", err) + } + + // Ensure value is encrypted in Redis (not plain JSON/token). + rawEncrypted, err := storage.client.HGet(ctx, sessionKey(sessionID), fieldCodexOAuth).Result() + if err != nil { + t.Fatalf("read encrypted value: %v", err) + } + if rawEncrypted == "" { + t.Fatal("expected encrypted value to be stored") + } + if rawEncrypted == data.AccessToken || rawEncrypted == data.IdToken || rawEncrypted == data.RefreshToken { + t.Fatal("expected stored value to be encrypted, got plain token") + } + + got, err := storage.GetSessionCodexOAuth(ctx, sessionID) + if err != nil { + t.Fatalf("get codex oauth: %v", err) + } + if got == nil { + t.Fatal("expected codex oauth data, got nil") + } + if got.AccessToken != data.AccessToken || got.IdToken != data.IdToken || got.RefreshToken != data.RefreshToken { + t.Fatalf("unexpected oauth data: %+v", got) + } + if got.ExpiresAt == nil || !got.ExpiresAt.Equal(expiresAt) { + t.Fatalf("unexpected expiresAt: got=%v want=%v", got.ExpiresAt, expiresAt) + } + + if err := storage.DeleteSessionCodexOAuth(ctx, sessionID); err != nil { + t.Fatalf("delete codex oauth: %v", err) + } + got, err = storage.GetSessionCodexOAuth(ctx, sessionID) + if err != nil { + t.Fatalf("get after delete: %v", err) + } + if got != nil { + t.Fatalf("expected nil after delete, got %+v", got) + } +} + func TestGetStreamEntriesByTypes(t *testing.T) { mr, storage := setupTestRedis(t) defer mr.Close() diff --git a/services/control-plane/internal/storage/storage.go b/services/control-plane/internal/storage/storage.go index c7149962..f9231d86 100644 --- a/services/control-plane/internal/storage/storage.go +++ b/services/control-plane/internal/storage/storage.go @@ -3,6 +3,7 @@ package storage import ( "context" "encoding/json" + "time" pb "github.com/angristan/netclode/services/control-plane/gen/netclode/v1" "github.com/redis/go-redis/v9" @@ -23,6 +24,15 @@ type StreamEntryWithID struct { Entry *StreamEntry } +// CodexOAuthSessionData is encrypted and stored per session for Codex OAuth mode. +type CodexOAuthSessionData struct { + AccessToken string `json:"access_token"` + IdToken string `json:"id_token"` + RefreshToken string `json:"refresh_token"` + ExpiresAt *time.Time `json:"expires_at,omitempty"` + UpdatedAt time.Time `json:"updated_at"` +} + // Storage defines the interface for session persistence. type Storage interface { // Sessions @@ -31,6 +41,12 @@ type Storage interface { GetAllSessions(ctx context.Context) ([]*pb.Session, error) UpdateSessionStatus(ctx context.Context, id string, status pb.SessionStatus) error UpdateSessionField(ctx context.Context, id, field, value string) error + SaveSessionCodexOAuth(ctx context.Context, sessionID string, data *CodexOAuthSessionData) error + GetSessionCodexOAuth(ctx context.Context, sessionID string) (*CodexOAuthSessionData, error) + DeleteSessionCodexOAuth(ctx context.Context, sessionID string) error + SaveCodexOAuth(ctx context.Context, data *CodexOAuthSessionData) error + GetCodexOAuth(ctx context.Context) (*CodexOAuthSessionData, error) + DeleteCodexOAuth(ctx context.Context) error DeleteSession(ctx context.Context, id string) error // Unified Stream (replaces messages, events, and notifications)