From 37b6088a10ed45017bb7701618e138c31afdb0ca Mon Sep 17 00:00:00 2001 From: Bazyli Brzoska Date: Thu, 5 Feb 2026 23:09:28 -0800 Subject: [PATCH 01/21] feat(ansible): add optional self-hosted MinIO storage with secure defaults and JuiceFS auto-wiring ## Summary Add first-class support for running MinIO on the Netclode host as an optional S3-compatible backend for JuiceFS. It keeps the current external S3 flow intact, and introduces an easier path where users can omit manual `DO_SPACES_*` / `JUICEFS_BUCKET` values when `MINIO_ENABLED=true`. ## What changed ### 1. New optional Ansible `minio` role - Installs MinIO server + `mc` client - Creates service user, directories, and systemd unit - Boots MinIO and creates the JuiceFS bucket (`netclode-juicefs` by default) - Credential resolution order: 1. explicit Ansible vars (`minio_root_user`/`minio_root_password`) 2. `.env` fallback (`DO_SPACES_*`) 3. generated credentials persisted in `/var/secrets/minio-root-user` and `/var/secrets/minio-root-password` ### 2. Security hardening for MinIO deployment - MinIO role now asserts `nftables` is active before deploying listeners - Updated docs/examples to run MinIO with firewall tag: `--tags "nftables,minio"` ### 3. `site.yaml` integration - Added optional MinIO role gated by `minio_enabled` (default `false`) ### 4. New MinIO settings in Ansible vars - `MINIO_ENABLED` - `MINIO_BUCKET_NAME` - `MINIO_API_PORT` - `MINIO_CONSOLE_PORT` ### 5. `deploy-secrets` auto-wiring (MinIO mode) When `MINIO_ENABLED=true` and S3 vars are omitted: - reads MinIO creds from `/var/secrets/minio-root-*` - auto-derives `JUICEFS_BUCKET` from host IP + MinIO port/bucket - defaults `JUICEFS_META_URL` if missing - then creates `juicefs-secret` as before ### 6. More robust `.env` parsing - Updated parsing in `deploy-secrets` to handle missing keys safely (no crash on absent vars) ### 7. Documentation updates - `docs/deployment.md`: now documents both storage paths: - external S3 (manual) - local MinIO (automatic) - `infra/ansible/README.md` and `.env.example` updated accordingly ## Backward compatibility - External/custom S3 configuration remains supported exactly as today. - MinIO is opt-in (`MINIO_ENABLED=false` by default). - Existing `scripts/deploy-secrets.sh` behavior is unchanged (still expects manual S3 env vars). ## Validation performed - `ansible-playbook` syntax checks passed - Deployed MinIO to `aibox` - Verified services: - `minio` active - `nftables` active - Verified external internet cannot reach MinIO: - `:9000` and `:9001` time out from outside the host - Verified MinIO bucket exists and health endpoint works locally on host --- .env.example | 11 +- docs/deployment.md | 17 +- infra/ansible/README.md | 41 ++- infra/ansible/group_vars/all.yaml | 7 + infra/ansible/playbooks/site.yaml | 4 + .../roles/deploy-secrets/tasks/main.yaml | 99 ++++++- infra/ansible/roles/minio/defaults/main.yaml | 15 + infra/ansible/roles/minio/tasks/main.yaml | 273 ++++++++++++++++++ .../roles/minio/templates/minio.env.j2 | 4 + .../roles/minio/templates/minio.service.j2 | 22 ++ 10 files changed, 471 insertions(+), 22 deletions(-) create mode 100644 infra/ansible/roles/minio/defaults/main.yaml create mode 100644 infra/ansible/roles/minio/tasks/main.yaml create mode 100644 infra/ansible/roles/minio/templates/minio.env.j2 create mode 100644 infra/ansible/roles/minio/templates/minio.service.j2 diff --git a/.env.example b/.env.example index 29c35b23..ca918806 100644 --- a/.env.example +++ b/.env.example @@ -15,12 +15,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/docs/deployment.md b/docs/deployment.md index 89b65f1e..e892795f 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -5,7 +5,9 @@ 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) - Ansible installed locally @@ -54,10 +56,17 @@ ANTHROPIC_API_KEY=sk-ant-api03-xxx 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 +78,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 diff --git a/infra/ansible/README.md b/infra/ansible/README.md index a2e001cc..ab9b67a6 100644 --- a/infra/ansible/README.md +++ b/infra/ansible/README.md @@ -52,7 +52,9 @@ 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 @@ -66,10 +68,17 @@ ANTHROPIC_API_KEY=sk-ant-api03-xxx 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 +107,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 +161,9 @@ ansible-playbook playbooks/site.yaml --skip-tags k8s-manifests # Deploy only k8s manifests (fast updates) ansible-playbook playbooks/k8s-only.yaml + +# Install/update MinIO only +MINIO_ENABLED=true ansible-playbook playbooks/site.yaml --tags "nftables,minio" ``` ### Local kubectl Access @@ -160,6 +194,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 +218,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) | 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/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/deploy-secrets/tasks/main.yaml b/infra/ansible/roles/deploy-secrets/tasks/main.yaml index 23196893..29c93348 100644 --- a/infra/ansible/roles/deploy-secrets/tasks/main.yaml +++ b/infra/ansible/roles/deploy-secrets/tasks/main.yaml @@ -29,20 +29,23 @@ - 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] }}" @@ -52,7 +55,7 @@ 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 @@ -195,12 +198,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 +289,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/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 From 83809f2e38bc37f2d156009931f51dbe75255347 Mon Sep 17 00:00:00 2001 From: Bazyli Brzoska Date: Fri, 6 Feb 2026 00:54:05 -0800 Subject: [PATCH 02/21] docs(ios): add instructions on Apple Certificates --- Makefile | 30 ++++++++++++++++++++++++++---- clients/ios/README.md | 28 ++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 4 deletions(-) diff --git a/Makefile b/Makefile index 00282202..c851aeda 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,26 @@ CONTEXT ?= netclode NAMESPACE ?= netclode -.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 deploy test-ios run-macos run-ios run-device print-ios-team-id proto proto-lint proto-breaking proto-setup # Proto generation proto: proto-setup ## Generate code from proto files @@ -54,20 +73,23 @@ 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/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: From 77e6bcd57514fc4aaf4a83da8d5388e87695bcc6 Mon Sep 17 00:00:00 2001 From: Bazyli Brzoska Date: Fri, 6 Feb 2026 15:55:16 -0800 Subject: [PATCH 03/21] fix(ios): reconnection after changing the URL MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Updated `/Volumes/Projects/SoftwareReferences/netclode/clients/ios/Netclode/Services/ConnectService.swift` so `connect(to:connectPort:)` always applies the new URL/port, even when state is `.connecting`/`.reconnecting`. - It now: - cancels any active reconnect task, - tears down current stream/client state, - resets to `.disconnected(reason: .userInitiated)`, - starts a fresh `performConnect()`. Why this fixes the issue: - Previously, `connect()` returned early unless state was `.disconnected`/`.suspended`, so changing host while “Reconnecting” kept retrying the old endpoint. --- .../Netclode/Services/ConnectService.swift | 23 +++++++++++-------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/clients/ios/Netclode/Services/ConnectService.swift b/clients/ios/Netclode/Services/ConnectService.swift index fba80d62..f2aa78a7 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() } From 5afe0eac0e80a14646c18f5f0d8c4c0e3e3099f7 Mon Sep 17 00:00:00 2001 From: Bazyli Brzoska Date: Fri, 6 Feb 2026 20:00:52 -0800 Subject: [PATCH 04/21] fix(ansible): make k8s-only self-sufficient for k8s-manifests deploys k8s-only deploys were failing in real runs because the playbook disabled fact gathering and did not load shared vars from group_vars/all.yaml (where k3s_kubeconfig is defined). Update playbooks/k8s-only.yaml to enable gather_facts and load ../group_vars/all.yaml via vars_files so the k8s-manifests role works standalone with image overrides. --- infra/ansible/playbooks/k8s-only.yaml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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 From a3339e6a444e92f61f299c2ae75dd83bb9a8251b Mon Sep 17 00:00:00 2001 From: Bazyli Brzoska Date: Fri, 6 Feb 2026 20:51:48 -0800 Subject: [PATCH 05/21] fix(ansible): ensure Helm is installed --- infra/ansible/roles/cilium/tasks/main.yaml | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) 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 From 1e1cfdd4e5672c7d7c17a27b81ff00d6131023ca Mon Sep 17 00:00:00 2001 From: Bazyli Brzoska Date: Fri, 6 Feb 2026 21:17:14 -0800 Subject: [PATCH 06/21] feat: backend-managed Codex OAuth flow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit replaces legacy env-based Codex OAuth token handling with a backend-managed OAuth flow, stores refresh-capable credentials encrypted at rest, refreshes and pushes short-lived tokens to running agents before prompt execution, and standardizes Codex model selection with explicit auth suffixes (`:oauth` / `:api`). Codex OAuth credentials were previously handled in ways that were hard to rotate safely and could lead to stale/mixed runtime auth behavior across warm and non-warm sandboxes. This PR centralizes OAuth state in control-plane storage, removes refresh tokens from sandbox exposure, and makes auth mode selection explicit and predictable. - Added backend Codex auth messages to client protocol: - `CodexAuthStart` - `CodexAuthStatus` - `CodexAuthLogout` - Added control-plane → agent runtime message: - `UpdateCodexAuth` (access/id token + optional expiry) - Removed `codex_refresh_token` from `SessionConfig`. - `CreateSessionRequest.codex_oauth_tokens` is now rejected; backend flow is required. - Codex session creation now requires model auth suffix (`:oauth` or `:api`). - Implemented backend-managed Codex device flow orchestration. - Added encrypted Redis storage for Codex OAuth data. - Introduced `CODEX_OAUTH_ENCRYPTION_KEY_B64` (must decode to 32 bytes). - Added fail-fast validation in `StartCodexAuth` when encryption key is missing/invalid. - Added refresh logic before prompt execution for OAuth sessions. - Sends refreshed short-lived tokens to connected agents via `UpdateCodexAuth`. - `fetchCodexModels` now determines `:oauth` model availability from stored OAuth state (not legacy env tokens). - Removed Codex secret-proxy placeholder mapping from control-plane host allowlist logic. - Added handling for `updateCodexAuth` control-plane message. - Codex adapter can update OAuth credentials at runtime without restart. - Improved auth mode resolution: - respects explicit model suffix - ignores placeholder API keys (`NETCLODE_PLACEHOLDER_*`) - OAuth auth file writing updated to expected structure for Codex usage. - No refresh token is written to sandbox auth file. - `netclode auth codex` now uses backend flow end-to-end. - Added: - `netclode auth codex status` - `netclode auth codex logout` - Removed `--print-env` flag (it was a no-op after backend flow migration). - `sessions create` validates Codex model auth suffix. - Secrets deployment migrated from legacy `CODEX_ACCESS_TOKEN` / `CODEX_ID_TOKEN` / `CODEX_REFRESH_TOKEN` to `CODEX_OAUTH_ENCRYPTION_KEY_B64`. - Updated control-plane manifest env wiring for encryption key. - Added templated image overrides for control-plane/agent manifests and optional image pull secret wiring. - Ensured control-plane receives `AGENT_IMAGE` for correct non-warm session image selection. - Added sandbox template templating in Ansible deployment path. - Updated deployment/SDK/CLI/Ansible docs to reflect backend-managed OAuth flow. - Updated Codex session examples to explicit model format (e.g. `gpt-5-codex:oauth:high`). - Updated secret-proxy docs to reflect that Codex is no longer placeholder-injected. - Codex model selection now requires explicit auth mode suffix. - `CreateSession` no longer accepts direct Codex OAuth tokens from clients. - Legacy Codex token env vars are no longer used for runtime OAuth. - `CODEX_OAUTH_ENCRYPTION_KEY_B64` is required for Codex OAuth usage. - `netclode auth codex --print-env` was removed. - Codex `:api` mode remains supported and unchanged in principle (uses `OPENAI_API_KEY`). - Codex `:oauth` mode now depends on backend-authenticated and encrypted stored credentials. - iOS protocol stubs are updated; Codex auth messages are currently ignored in `ConnectService` (no new UI flow yet). - Added/updated unit coverage for: - OAuth config requirements on create/start - prompt-time OAuth refresh + push to agent - Codex proxy mapping behavior - Ran: - `go test ./services/control-plane/internal/session ./services/control-plane/internal/api ./services/control-plane/internal/storage` - `go test ./clients/cli/...` 1. Set `CODEX_OAUTH_ENCRYPTION_KEY_B64` in `.env` and deploy secrets. 2. Deploy manifests with updated control-plane/agent images. 3. Verify control-plane logs show expected Codex config state. 4. Run `netclode auth codex`, then `netclode auth codex status`. 5. Create a Codex session with explicit auth suffix and validate prompt execution. - Missing/invalid encryption key blocks OAuth start (intentional fail-fast behavior). - Any old tooling depending on env token export or missing auth suffix will need updates. - Add dedicated iOS UX for backend Codex OAuth start/status/logout. - Consider clearer server-side error messaging when auth suffix is omitted in non-CLI clients. --- .env.example | 12 +- clients/cli/README.md | 27 +- clients/cli/cmd/auth.go | 127 +- clients/cli/cmd/sessions.go | 28 + clients/cli/internal/client/client.go | 76 +- clients/cli/internal/codex/oauth.go | 1 + clients/cli/internal/codex/store.go | 176 +++ .../Generated/netclode/v1/agent.pb.swift | 128 +- .../Generated/netclode/v1/client.pb.swift | 1018 +++++++++++-- .../Generated/netclode/v1/common.pb.swift | 174 +-- .../Generated/netclode/v1/events.pb.swift | 40 +- .../Netclode/Services/ConnectService.swift | 11 +- docs/deployment.md | 73 +- docs/sdk-support.md | 18 +- docs/secret-proxy.md | 3 +- infra/ansible/README.md | 23 +- infra/ansible/playbooks/secrets.yaml | 3 +- .../roles/deploy-secrets/tasks/main.yaml | 15 +- .../roles/k8s-manifests/defaults/main.yaml | 6 + .../roles/k8s-manifests/tasks/main.yaml | 37 +- .../templates/control-plane.yaml.j2 | 25 +- .../templates/sandbox-template.yaml.j2 | 239 ++++ infra/k8s/sandbox-template.yaml | 18 - proto/netclode/v1/agent.proto | 11 +- proto/netclode/v1/client.proto | 59 + proto/netclode/v1/common.proto | 1 - services/agent/Dockerfile | 1 + services/agent/gen/netclode/v1/agent_pb.ts | 41 +- services/agent/gen/netclode/v1/client_pb.ts | 361 ++++- services/agent/gen/netclode/v1/common_pb.ts | 7 +- services/agent/src/connect-client.ts | 32 +- services/agent/src/sdk/codex/adapter.test.ts | 57 + services/agent/src/sdk/codex/adapter.ts | 102 +- services/agent/src/sdk/types.ts | 7 +- .../control-plane/gen/netclode/v1/agent.pb.go | 147 +- .../gen/netclode/v1/agent_grpc.pb.go | 2 +- .../gen/netclode/v1/client.pb.go | 1265 +++++++++++++---- .../gen/netclode/v1/client_grpc.pb.go | 2 +- .../gen/netclode/v1/common.pb.go | 28 +- .../internal/api/connect_agent.go | 49 +- .../internal/api/connect_client.go | 74 +- .../control-plane/internal/config/config.go | 23 +- .../control-plane/internal/session/agent.go | 20 + .../internal/session/codex_auth_backend.go | 353 +++++ .../internal/session/codex_oauth.go | 169 +++ .../control-plane/internal/session/manager.go | 197 ++- .../internal/session/manager_test.go | 287 +++- .../control-plane/internal/session/state.go | 4 + .../control-plane/internal/storage/redis.go | 223 ++- .../internal/storage/redis_test.go | 60 + .../control-plane/internal/storage/storage.go | 16 + 51 files changed, 5013 insertions(+), 863 deletions(-) create mode 100644 clients/cli/internal/codex/store.go create mode 100644 infra/ansible/roles/k8s-manifests/templates/sandbox-template.yaml.j2 create mode 100644 services/agent/src/sdk/codex/adapter.test.ts create mode 100644 services/control-plane/internal/session/codex_auth_backend.go create mode 100644 services/control-plane/internal/session/codex_oauth.go diff --git a/.env.example b/.env.example index ca918806..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 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/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/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..3f4c9f4f 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,31 @@ 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 unknownFields = SwiftProtobuf.UnknownStorage() public enum OneOf_Message: Equatable, Sendable { @@ -235,6 +306,10 @@ 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) } @@ -414,6 +489,31 @@ 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 unknownFields = SwiftProtobuf.UnknownStorage() public enum OneOf_Message: Equatable, Sendable { @@ -442,6 +542,10 @@ 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) } @@ -464,6 +568,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 +603,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 +626,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 +717,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 +726,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 +747,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 +759,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 +792,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 +815,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 +838,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 +861,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 +882,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 +907,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 +930,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 +955,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 +982,11 @@ 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 {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 +1007,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 +1028,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 +1049,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 +1072,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 +1084,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 +1106,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 +1119,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 +1152,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 +1173,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 +1196,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 +1221,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 +1247,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 {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} + + 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 {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 +1331,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 +1363,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 +1386,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 +1409,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 +1432,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 +1453,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 +1517,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 +1550,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 +1582,11 @@ 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 {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 +1605,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 +1630,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 +1655,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 +1679,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 +1714,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 +1747,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 +1783,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 +1913,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 +1939,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 +1968,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 +1995,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 +2030,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 +2049,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") public mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { @@ -2007,6 +2349,45 @@ extension Netclode_V1_ClientMessage: SwiftProtobuf.Message, SwiftProtobuf._Messa self.message = .getResourceLimits(v) } }() + 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) + } + }() default: break } } @@ -2106,6 +2487,18 @@ 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 nil: break } try unknownFields.traverse(visitor: &visitor) @@ -2120,7 +2513,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") public mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { @@ -2388,6 +2781,45 @@ 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) + } + }() default: break } } @@ -2479,6 +2911,18 @@ 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 nil: break } try unknownFields.traverse(visitor: &visitor) @@ -2521,9 +2965,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 +3034,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 +3075,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 +3092,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 } @@ -3207,7 +3705,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 +3716,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 +3736,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 +3746,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 +3947,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") @@ -4163,6 +4768,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..7f3dbd63 100644 --- a/clients/ios/Netclode/Generated/netclode/v1/events.pb.swift +++ b/clients/ios/Netclode/Generated/netclode/v1/events.pb.swift @@ -346,11 +346,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 +370,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 +405,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 +442,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 +490,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} diff --git a/clients/ios/Netclode/Services/ConnectService.swift b/clients/ios/Netclode/Services/ConnectService.swift index f2aa78a7..cfca95e9 100644 --- a/clients/ios/Netclode/Services/ConnectService.swift +++ b/clients/ios/Netclode/Services/ConnectService.swift @@ -548,6 +548,15 @@ final class ConnectService { defaultMemoryMB: msg.defaultMemoryMb )) + case .codexAuthStarted: + return nil + + case .codexAuthStatus: + return nil + + case .codexAuthLoggedOut: + return nil + case .none: return nil } @@ -1266,7 +1275,7 @@ final class ConnectService { private func recordActivity() { lastActivityAt = Date() } - + private func convertToProtoMessage(_ message: ClientMessage) -> Netclode_V1_ClientMessage { var proto = Netclode_V1_ClientMessage() diff --git a/docs/deployment.md b/docs/deployment.md index e892795f..5d7cda75 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -9,7 +9,9 @@ Here's how to get Netclode running on your own server. - 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 @@ -39,18 +41,31 @@ 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 @@ -91,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: @@ -112,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: @@ -144,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 @@ -174,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): @@ -181,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: 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/infra/ansible/README.md b/infra/ansible/README.md index ab9b67a6..8bdb3c15 100644 --- a/infra/ansible/README.md +++ b/infra/ansible/README.md @@ -59,10 +59,14 @@ 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 @@ -162,6 +166,20 @@ 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= + # Install/update MinIO only MINIO_ENABLED=true ansible-playbook playbooks/site.yaml --tags "nftables,minio" ``` @@ -421,7 +439,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/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/roles/deploy-secrets/tasks/main.yaml b/infra/ansible/roles/deploy-secrets/tasks/main.yaml index 29c93348..17694be8 100644 --- a/infra/ansible/roles/deploy-secrets/tasks/main.yaml +++ b/infra/ansible/roles/deploy-secrets/tasks/main.yaml @@ -49,9 +49,7 @@ 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] }}" @@ -126,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) @@ -166,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" @@ -176,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 }}" @@ -187,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: 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/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..955d67a8 100644 --- a/proto/netclode/v1/client.proto +++ b/proto/netclode/v1/client.proto @@ -41,6 +41,10 @@ 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; } } @@ -72,6 +76,10 @@ 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; } } @@ -87,6 +95,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 +114,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 { @@ -183,6 +200,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 +228,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 // ============================================================================ @@ -303,6 +333,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/services/agent/Dockerfile b/services/agent/Dockerfile index 3ec3a95b..8ab224c7 100644 --- a/services/agent/Dockerfile +++ b/services/agent/Dockerfile @@ -59,6 +59,7 @@ COPY services/agent services/agent # Install agent dependencies WORKDIR /build/services/agent RUN --mount=type=cache,target=/root/.npm npm install +RUN npm rebuild node-pty --build-from-source # Bundle agent RUN esbuild src/index.ts --bundle --platform=node --format=esm --packages=external --outfile=dist/agent.js 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..2f487a8c 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("ChhuZXRjbG9kZS92MS9jbGllbnQucHJvdG8SC25ldGNsb2RlLnYxIooMCg1DbGllbnRNZXNzYWdlEjsKDmNyZWF0ZV9zZXNzaW9uGAEgASgLMiEubmV0Y2xvZGUudjEuQ3JlYXRlU2Vzc2lvblJlcXVlc3RIABI5Cg1saXN0X3Nlc3Npb25zGAIgASgLMiAubmV0Y2xvZGUudjEuTGlzdFNlc3Npb25zUmVxdWVzdEgAEjcKDG9wZW5fc2Vzc2lvbhgDIAEoCzIfLm5ldGNsb2RlLnYxLk9wZW5TZXNzaW9uUmVxdWVzdEgAEjsKDnJlc3VtZV9zZXNzaW9uGAQgASgLMiEubmV0Y2xvZGUudjEuUmVzdW1lU2Vzc2lvblJlcXVlc3RIABI5Cg1wYXVzZV9zZXNzaW9uGAUgASgLMiAubmV0Y2xvZGUudjEuUGF1c2VTZXNzaW9uUmVxdWVzdEgAEjsKDmRlbGV0ZV9zZXNzaW9uGAYgASgLMiEubmV0Y2xvZGUudjEuRGVsZXRlU2Vzc2lvblJlcXVlc3RIABJEChNkZWxldGVfYWxsX3Nlc3Npb25zGAcgASgLMiUubmV0Y2xvZGUudjEuRGVsZXRlQWxsU2Vzc2lvbnNSZXF1ZXN0SAASNQoLc2VuZF9wcm9tcHQYCCABKAsyHi5uZXRjbG9kZS52MS5TZW5kUHJvbXB0UmVxdWVzdEgAEj8KEGludGVycnVwdF9wcm9tcHQYCSABKAsyIy5uZXRjbG9kZS52MS5JbnRlcnJ1cHRQcm9tcHRSZXF1ZXN0SAASOwoOdGVybWluYWxfaW5wdXQYCiABKAsyIS5uZXRjbG9kZS52MS5UZXJtaW5hbElucHV0UmVxdWVzdEgAEj0KD3Rlcm1pbmFsX3Jlc2l6ZRgLIAEoCzIiLm5ldGNsb2RlLnYxLlRlcm1pbmFsUmVzaXplUmVxdWVzdEgAEjUKC2V4cG9zZV9wb3J0GAwgASgLMh4ubmV0Y2xvZGUudjEuRXhwb3NlUG9ydFJlcXVlc3RIABIoCgRzeW5jGA0gASgLMhgubmV0Y2xvZGUudjEuU3luY1JlcXVlc3RIABJAChFsaXN0X2dpdGh1Yl9yZXBvcxgOIAEoCzIjLm5ldGNsb2RlLnYxLkxpc3RHaXRIdWJSZXBvc1JlcXVlc3RIABIzCgpnaXRfc3RhdHVzGA8gASgLMh0ubmV0Y2xvZGUudjEuR2l0U3RhdHVzUmVxdWVzdEgAEi8KCGdpdF9kaWZmGBAgASgLMhsubmV0Y2xvZGUudjEuR2l0RGlmZlJlcXVlc3RIABI1CgtsaXN0X21vZGVscxgRIAEoCzIeLm5ldGNsb2RlLnYxLkxpc3RNb2RlbHNSZXF1ZXN0SAASQgoSZ2V0X2NvcGlsb3Rfc3RhdHVzGBIgASgLMiQubmV0Y2xvZGUudjEuR2V0Q29waWxvdFN0YXR1c1JlcXVlc3RIABI7Cg5saXN0X3NuYXBzaG90cxgTIAEoCzIhLm5ldGNsb2RlLnYxLkxpc3RTbmFwc2hvdHNSZXF1ZXN0SAASPwoQcmVzdG9yZV9zbmFwc2hvdBgUIAEoCzIjLm5ldGNsb2RlLnYxLlJlc3RvcmVTbmFwc2hvdFJlcXVlc3RIABJCChJ1cGRhdGVfcmVwb19hY2Nlc3MYFSABKAsyJC5uZXRjbG9kZS52MS5VcGRhdGVSZXBvQWNjZXNzUmVxdWVzdEgAEkQKE2dldF9yZXNvdXJjZV9saW1pdHMYFiABKAsyJS5uZXRjbG9kZS52MS5HZXRSZXNvdXJjZUxpbWl0c1JlcXVlc3RIABI+ChBjb2RleF9hdXRoX3N0YXJ0GBcgASgLMiIubmV0Y2xvZGUudjEuQ29kZXhBdXRoU3RhcnRSZXF1ZXN0SAASQAoRY29kZXhfYXV0aF9zdGF0dXMYGCABKAsyIy5uZXRjbG9kZS52MS5Db2RleEF1dGhTdGF0dXNSZXF1ZXN0SAASQAoRY29kZXhfYXV0aF9sb2dvdXQYGSABKAsyIy5uZXRjbG9kZS52MS5Db2RleEF1dGhMb2dvdXRSZXF1ZXN0SABCCQoHbWVzc2FnZSKYCwoNU2VydmVyTWVzc2FnZRI+Cg9zZXNzaW9uX2NyZWF0ZWQYASABKAsyIy5uZXRjbG9kZS52MS5TZXNzaW9uQ3JlYXRlZFJlc3BvbnNlSAASPgoPc2Vzc2lvbl91cGRhdGVkGAIgASgLMiMubmV0Y2xvZGUudjEuU2Vzc2lvblVwZGF0ZWRSZXNwb25zZUgAEj4KD3Nlc3Npb25fZGVsZXRlZBgDIAEoCzIjLm5ldGNsb2RlLnYxLlNlc3Npb25EZWxldGVkUmVzcG9uc2VIABJHChRzZXNzaW9uc19kZWxldGVkX2FsbBgEIAEoCzInLm5ldGNsb2RlLnYxLlNlc3Npb25zRGVsZXRlZEFsbFJlc3BvbnNlSAASOAoMc2Vzc2lvbl9saXN0GAUgASgLMiAubmV0Y2xvZGUudjEuU2Vzc2lvbkxpc3RSZXNwb25zZUgAEjoKDXNlc3Npb25fc3RhdGUYBiABKAsyIS5uZXRjbG9kZS52MS5TZXNzaW9uU3RhdGVSZXNwb25zZUgAEjIKDXN5bmNfcmVzcG9uc2UYByABKAsyGS5uZXRjbG9kZS52MS5TeW5jUmVzcG9uc2VIABI4CgxzdHJlYW1fZW50cnkYCCABKAsyIC5uZXRjbG9kZS52MS5TdHJlYW1FbnRyeVJlc3BvbnNlSAASOAoMcG9ydF9leHBvc2VkGA0gASgLMiAubmV0Y2xvZGUudjEuUG9ydEV4cG9zZWRSZXNwb25zZUgAEjgKDGdpdGh1Yl9yZXBvcxgOIAEoCzIgLm5ldGNsb2RlLnYxLkdpdEh1YlJlcG9zUmVzcG9uc2VIABI0CgpnaXRfc3RhdHVzGA8gASgLMh4ubmV0Y2xvZGUudjEuR2l0U3RhdHVzUmVzcG9uc2VIABIwCghnaXRfZGlmZhgQIAEoCzIcLm5ldGNsb2RlLnYxLkdpdERpZmZSZXNwb25zZUgAEisKBWVycm9yGBEgASgLMhoubmV0Y2xvZGUudjEuRXJyb3JSZXNwb25zZUgAEi0KBm1vZGVscxgSIAEoCzIbLm5ldGNsb2RlLnYxLk1vZGVsc1Jlc3BvbnNlSAASPAoOY29waWxvdF9zdGF0dXMYEyABKAsyIi5uZXRjbG9kZS52MS5Db3BpbG90U3RhdHVzUmVzcG9uc2VIABJAChBzbmFwc2hvdF9jcmVhdGVkGBQgASgLMiQubmV0Y2xvZGUudjEuU25hcHNob3RDcmVhdGVkUmVzcG9uc2VIABI6Cg1zbmFwc2hvdF9saXN0GBUgASgLMiEubmV0Y2xvZGUudjEuU25hcHNob3RMaXN0UmVzcG9uc2VIABJCChFzbmFwc2hvdF9yZXN0b3JlZBgWIAEoCzIlLm5ldGNsb2RlLnYxLlNuYXBzaG90UmVzdG9yZWRSZXNwb25zZUgAEkUKE3JlcG9fYWNjZXNzX3VwZGF0ZWQYFyABKAsyJi5uZXRjbG9kZS52MS5SZXBvQWNjZXNzVXBkYXRlZFJlc3BvbnNlSAASPgoPcmVzb3VyY2VfbGltaXRzGBggASgLMiMubmV0Y2xvZGUudjEuUmVzb3VyY2VMaW1pdHNSZXNwb25zZUgAEkMKEmNvZGV4X2F1dGhfc3RhcnRlZBgZIAEoCzIlLm5ldGNsb2RlLnYxLkNvZGV4QXV0aFN0YXJ0ZWRSZXNwb25zZUgAEkEKEWNvZGV4X2F1dGhfc3RhdHVzGBogASgLMiQubmV0Y2xvZGUudjEuQ29kZXhBdXRoU3RhdHVzUmVzcG9uc2VIABJIChVjb2RleF9hdXRoX2xvZ2dlZF9vdXQYGyABKAsyJy5uZXRjbG9kZS52MS5Db2RleEF1dGhMb2dnZWRPdXRSZXNwb25zZUgAQgkKB21lc3NhZ2UiJwoNTmV0d29ya0NvbmZpZxIWCg50YWlsbmV0X2FjY2VzcxgBIAEoCCKVAQoQQ29kZXhPQXV0aFRva2VucxIUCgxhY2Nlc3NfdG9rZW4YASABKAkSEAoIaWRfdG9rZW4YAiABKAkSFQoNcmVmcmVzaF90b2tlbhgDIAEoCRIzCgpleHBpcmVzX2F0GAQgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcEgAiAEBQg0KC19leHBpcmVzX2F0IusEChRDcmVhdGVTZXNzaW9uUmVxdWVzdBIXCgpyZXF1ZXN0X2lkGAEgASgJSACIAQESEQoEbmFtZRgCIAEoCUgBiAEBEg0KBXJlcG9zGAMgAygJEjEKC3JlcG9fYWNjZXNzGAQgASgOMhcubmV0Y2xvZGUudjEuUmVwb0FjY2Vzc0gCiAEBEhsKDmluaXRpYWxfcHJvbXB0GAUgASgJSAOIAQESKwoIc2RrX3R5cGUYBiABKA4yFC5uZXRjbG9kZS52MS5TZGtUeXBlSASIAQESEgoFbW9kZWwYByABKAlIBYgBARI5Cg9jb3BpbG90X2JhY2tlbmQYCCABKA4yGy5uZXRjbG9kZS52MS5Db3BpbG90QmFja2VuZEgGiAEBEjcKDm5ldHdvcmtfY29uZmlnGAkgASgLMhoubmV0Y2xvZGUudjEuTmV0d29ya0NvbmZpZ0gHiAEBEjUKCXJlc291cmNlcxgKIAEoCzIdLm5ldGNsb2RlLnYxLlNhbmRib3hSZXNvdXJjZXNICIgBARI+ChJjb2RleF9vYXV0aF90b2tlbnMYCyABKAsyHS5uZXRjbG9kZS52MS5Db2RleE9BdXRoVG9rZW5zSAmIAQFCDQoLX3JlcXVlc3RfaWRCBwoFX25hbWVCDgoMX3JlcG9fYWNjZXNzQhEKD19pbml0aWFsX3Byb21wdEILCglfc2RrX3R5cGVCCAoGX21vZGVsQhIKEF9jb3BpbG90X2JhY2tlbmRCEQoPX25ldHdvcmtfY29uZmlnQgwKCl9yZXNvdXJjZXNCFQoTX2NvZGV4X29hdXRoX3Rva2VucyI9ChNMaXN0U2Vzc2lvbnNSZXF1ZXN0EhcKCnJlcXVlc3RfaWQYASABKAlIAIgBAUINCgtfcmVxdWVzdF9pZCKgAQoST3BlblNlc3Npb25SZXF1ZXN0EhcKCnJlcXVlc3RfaWQYASABKAlIAIgBARISCgpzZXNzaW9uX2lkGAIgASgJEhwKD2FmdGVyX3N0cmVhbV9pZBgDIAEoCUgBiAEBEhIKBWxpbWl0GAQgASgFSAKIAQFCDQoLX3JlcXVlc3RfaWRCEgoQX2FmdGVyX3N0cmVhbV9pZEIICgZfbGltaXQiUgoUUmVzdW1lU2Vzc2lvblJlcXVlc3QSFwoKcmVxdWVzdF9pZBgBIAEoCUgAiAEBEhIKCnNlc3Npb25faWQYAiABKAlCDQoLX3JlcXVlc3RfaWQiUQoTUGF1c2VTZXNzaW9uUmVxdWVzdBIXCgpyZXF1ZXN0X2lkGAEgASgJSACIAQESEgoKc2Vzc2lvbl9pZBgCIAEoCUINCgtfcmVxdWVzdF9pZCJSChREZWxldGVTZXNzaW9uUmVxdWVzdBIXCgpyZXF1ZXN0X2lkGAEgASgJSACIAQESEgoKc2Vzc2lvbl9pZBgCIAEoCUINCgtfcmVxdWVzdF9pZCJCChhEZWxldGVBbGxTZXNzaW9uc1JlcXVlc3QSFwoKcmVxdWVzdF9pZBgBIAEoCUgAiAEBQg0KC19yZXF1ZXN0X2lkIl0KEVNlbmRQcm9tcHRSZXF1ZXN0EhcKCnJlcXVlc3RfaWQYASABKAlIAIgBARISCgpzZXNzaW9uX2lkGAIgASgJEgwKBHRleHQYAyABKAlCDQoLX3JlcXVlc3RfaWQiVAoWSW50ZXJydXB0UHJvbXB0UmVxdWVzdBIXCgpyZXF1ZXN0X2lkGAEgASgJSACIAQESEgoKc2Vzc2lvbl9pZBgCIAEoCUINCgtfcmVxdWVzdF9pZCJgChRUZXJtaW5hbElucHV0UmVxdWVzdBIXCgpyZXF1ZXN0X2lkGAEgASgJSACIAQESEgoKc2Vzc2lvbl9pZBgCIAEoCRIMCgRkYXRhGAMgASgJQg0KC19yZXF1ZXN0X2lkIm8KFVRlcm1pbmFsUmVzaXplUmVxdWVzdBIXCgpyZXF1ZXN0X2lkGAEgASgJSACIAQESEgoKc2Vzc2lvbl9pZBgCIAEoCRIMCgRjb2xzGAMgASgFEgwKBHJvd3MYBCABKAVCDQoLX3JlcXVlc3RfaWQiXQoRRXhwb3NlUG9ydFJlcXVlc3QSFwoKcmVxdWVzdF9pZBgBIAEoCUgAiAEBEhIKCnNlc3Npb25faWQYAiABKAkSDAoEcG9ydBgDIAEoBUINCgtfcmVxdWVzdF9pZCI1CgtTeW5jUmVxdWVzdBIXCgpyZXF1ZXN0X2lkGAEgASgJSACIAQFCDQoLX3JlcXVlc3RfaWQiQAoWTGlzdEdpdEh1YlJlcG9zUmVxdWVzdBIXCgpyZXF1ZXN0X2lkGAEgASgJSACIAQFCDQoLX3JlcXVlc3RfaWQiTgoQR2l0U3RhdHVzUmVxdWVzdBIXCgpyZXF1ZXN0X2lkGAEgASgJSACIAQESEgoKc2Vzc2lvbl9pZBgCIAEoCUINCgtfcmVxdWVzdF9pZCJoCg5HaXREaWZmUmVxdWVzdBIXCgpyZXF1ZXN0X2lkGAEgASgJSACIAQESEgoKc2Vzc2lvbl9pZBgCIAEoCRIRCgRmaWxlGAMgASgJSAGIAQFCDQoLX3JlcXVlc3RfaWRCBwoFX2ZpbGUi8AEKEUxpc3RNb2RlbHNSZXF1ZXN0EhcKCnJlcXVlc3RfaWQYASABKAlIAIgBARImCghzZGtfdHlwZRgCIAEoDjIULm5ldGNsb2RlLnYxLlNka1R5cGUSOQoPY29waWxvdF9iYWNrZW5kGAMgASgOMhsubmV0Y2xvZGUudjEuQ29waWxvdEJhY2tlbmRIAYgBARIiChVjb2RleF9vYXV0aF9hdmFpbGFibGUYBCABKAhIAogBAUINCgtfcmVxdWVzdF9pZEISChBfY29waWxvdF9iYWNrZW5kQhgKFl9jb2RleF9vYXV0aF9hdmFpbGFibGUiQQoXR2V0Q29waWxvdFN0YXR1c1JlcXVlc3QSFwoKcmVxdWVzdF9pZBgBIAEoCUgAiAEBQg0KC19yZXF1ZXN0X2lkIlIKFExpc3RTbmFwc2hvdHNSZXF1ZXN0EhcKCnJlcXVlc3RfaWQYASABKAlIAIgBARISCgpzZXNzaW9uX2lkGAIgASgJQg0KC19yZXF1ZXN0X2lkImkKFlJlc3RvcmVTbmFwc2hvdFJlcXVlc3QSFwoKcmVxdWVzdF9pZBgBIAEoCUgAiAEBEhIKCnNlc3Npb25faWQYAiABKAkSEwoLc25hcHNob3RfaWQYAyABKAlCDQoLX3JlcXVlc3RfaWQigwEKF1VwZGF0ZVJlcG9BY2Nlc3NSZXF1ZXN0EhcKCnJlcXVlc3RfaWQYASABKAlIAIgBARISCgpzZXNzaW9uX2lkGAIgASgJEiwKC3JlcG9fYWNjZXNzGAMgASgOMhcubmV0Y2xvZGUudjEuUmVwb0FjY2Vzc0INCgtfcmVxdWVzdF9pZCJCChhHZXRSZXNvdXJjZUxpbWl0c1JlcXVlc3QSFwoKcmVxdWVzdF9pZBgBIAEoCUgAiAEBQg0KC19yZXF1ZXN0X2lkIj8KFUNvZGV4QXV0aFN0YXJ0UmVxdWVzdBIXCgpyZXF1ZXN0X2lkGAEgASgJSACIAQFCDQoLX3JlcXVlc3RfaWQiQAoWQ29kZXhBdXRoU3RhdHVzUmVxdWVzdBIXCgpyZXF1ZXN0X2lkGAEgASgJSACIAQFCDQoLX3JlcXVlc3RfaWQiQAoWQ29kZXhBdXRoTG9nb3V0UmVxdWVzdBIXCgpyZXF1ZXN0X2lkGAEgASgJSACIAQFCDQoLX3JlcXVlc3RfaWQiZwoWU2Vzc2lvbkNyZWF0ZWRSZXNwb25zZRIlCgdzZXNzaW9uGAEgASgLMhQubmV0Y2xvZGUudjEuU2Vzc2lvbhIXCgpyZXF1ZXN0X2lkGAIgASgJSACIAQFCDQoLX3JlcXVlc3RfaWQiPwoWU2Vzc2lvblVwZGF0ZWRSZXNwb25zZRIlCgdzZXNzaW9uGAEgASgLMhQubmV0Y2xvZGUudjEuU2Vzc2lvbiJUChZTZXNzaW9uRGVsZXRlZFJlc3BvbnNlEhIKCnNlc3Npb25faWQYASABKAkSFwoKcmVxdWVzdF9pZBgCIAEoCUgAiAEBQg0KC19yZXF1ZXN0X2lkIlkKGlNlc3Npb25zRGVsZXRlZEFsbFJlc3BvbnNlEhMKC2RlbGV0ZWRfaWRzGAEgAygJEhcKCnJlcXVlc3RfaWQYAiABKAlIAIgBAUINCgtfcmVxdWVzdF9pZCJlChNTZXNzaW9uTGlzdFJlc3BvbnNlEiYKCHNlc3Npb25zGAEgAygLMhQubmV0Y2xvZGUudjEuU2Vzc2lvbhIXCgpyZXF1ZXN0X2lkGAIgASgJSACIAQFCDQoLX3JlcXVlc3RfaWQimgIKFFNlc3Npb25TdGF0ZVJlc3BvbnNlEiUKB3Nlc3Npb24YASABKAsyFC5uZXRjbG9kZS52MS5TZXNzaW9uEikKB2VudHJpZXMYAiADKAsyGC5uZXRjbG9kZS52MS5TdHJlYW1FbnRyeRIQCghoYXNfbW9yZRgDIAEoCBIbCg5sYXN0X3N0cmVhbV9pZBgEIAEoCUgAiAEBEjYKC2luX3Byb2dyZXNzGAUgASgLMhwubmV0Y2xvZGUudjEuSW5Qcm9ncmVzc1N0YXRlSAGIAQESFwoKcmVxdWVzdF9pZBgGIAEoCUgCiAEBQhEKD19sYXN0X3N0cmVhbV9pZEIOCgxfaW5fcHJvZ3Jlc3NCDQoLX3JlcXVlc3RfaWQilgEKDFN5bmNSZXNwb25zZRItCghzZXNzaW9ucxgBIAMoCzIbLm5ldGNsb2RlLnYxLlNlc3Npb25TdW1tYXJ5Ei8KC3NlcnZlcl90aW1lGAIgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcBIXCgpyZXF1ZXN0X2lkGAMgASgJSACIAQFCDQoLX3JlcXVlc3RfaWQiUgoTU3RyZWFtRW50cnlSZXNwb25zZRISCgpzZXNzaW9uX2lkGAEgASgJEicKBWVudHJ5GAIgASgLMhgubmV0Y2xvZGUudjEuU3RyZWFtRW50cnkidAoTUG9ydEV4cG9zZWRSZXNwb25zZRISCgpzZXNzaW9uX2lkGAEgASgJEgwKBHBvcnQYAiABKAUSEwoLcHJldmlld191cmwYAyABKAkSFwoKcmVxdWVzdF9pZBgEIAEoCUgAiAEBQg0KC19yZXF1ZXN0X2lkImUKE0dpdEh1YlJlcG9zUmVzcG9uc2USJgoFcmVwb3MYASADKAsyFy5uZXRjbG9kZS52MS5HaXRIdWJSZXBvEhcKCnJlcXVlc3RfaWQYAiABKAlIAIgBAUINCgtfcmVxdWVzdF9pZCJ6ChFHaXRTdGF0dXNSZXNwb25zZRISCgpzZXNzaW9uX2lkGAEgASgJEikKBWZpbGVzGAIgAygLMhoubmV0Y2xvZGUudjEuR2l0RmlsZUNoYW5nZRIXCgpyZXF1ZXN0X2lkGAMgASgJSACIAQFCDQoLX3JlcXVlc3RfaWQiWwoPR2l0RGlmZlJlc3BvbnNlEhIKCnNlc3Npb25faWQYASABKAkSDAoEZGlmZhgCIAEoCRIXCgpyZXF1ZXN0X2lkGAMgASgJSACIAQFCDQoLX3JlcXVlc3RfaWQiWgoNRXJyb3JSZXNwb25zZRIhCgVlcnJvchgBIAEoCzISLm5ldGNsb2RlLnYxLkVycm9yEhcKCnJlcXVlc3RfaWQYAiABKAlIAIgBAUINCgtfcmVxdWVzdF9pZCKaAQoOTW9kZWxzUmVzcG9uc2USJgoGbW9kZWxzGAEgAygLMhYubmV0Y2xvZGUudjEuTW9kZWxJbmZvEhcKCnJlcXVlc3RfaWQYAiABKAlIAIgBARIrCghzZGtfdHlwZRgDIAEoDjIULm5ldGNsb2RlLnYxLlNka1R5cGVIAYgBAUINCgtfcmVxdWVzdF9pZEILCglfc2RrX3R5cGUirQEKFUNvcGlsb3RTdGF0dXNSZXNwb25zZRIsCgRhdXRoGAEgASgLMh4ubmV0Y2xvZGUudjEuQ29waWxvdEF1dGhTdGF0dXMSNAoFcXVvdGEYAiABKAsyIC5uZXRjbG9kZS52MS5Db3BpbG90UHJlbWl1bVF1b3RhSACIAQESFwoKcmVxdWVzdF9pZBgDIAEoCUgBiAEBQggKBl9xdW90YUINCgtfcmVxdWVzdF9pZCL/AQoYQ29kZXhBdXRoU3RhcnRlZFJlc3BvbnNlEhgKEHZlcmlmaWNhdGlvbl91cmkYASABKAkSJgoZdmVyaWZpY2F0aW9uX3VyaV9jb21wbGV0ZRgCIAEoCUgAiAEBEhEKCXVzZXJfY29kZRgDIAEoCRIYChBpbnRlcnZhbF9zZWNvbmRzGAQgASgFEi4KCmV4cGlyZXNfYXQYBSABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEhcKCnJlcXVlc3RfaWQYBiABKAlIAYgBAUIcChpfdmVyaWZpY2F0aW9uX3VyaV9jb21wbGV0ZUINCgtfcmVxdWVzdF9pZCL3AQoXQ29kZXhBdXRoU3RhdHVzUmVzcG9uc2USKgoFc3RhdGUYASABKA4yGy5uZXRjbG9kZS52MS5Db2RleEF1dGhTdGF0ZRIXCgphY2NvdW50X2lkGAIgASgJSACIAQESMwoKZXhwaXJlc19hdBgDIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXBIAYgBARISCgVlcnJvchgEIAEoCUgCiAEBEhcKCnJlcXVlc3RfaWQYBSABKAlIA4gBAUINCgtfYWNjb3VudF9pZEINCgtfZXhwaXJlc19hdEIICgZfZXJyb3JCDQoLX3JlcXVlc3RfaWQiRAoaQ29kZXhBdXRoTG9nZ2VkT3V0UmVzcG9uc2USFwoKcmVxdWVzdF9pZBgBIAEoCUgAiAEBQg0KC19yZXF1ZXN0X2lkIlYKF1NuYXBzaG90Q3JlYXRlZFJlc3BvbnNlEhIKCnNlc3Npb25faWQYASABKAkSJwoIc25hcHNob3QYAiABKAsyFS5uZXRjbG9kZS52MS5TbmFwc2hvdCJ8ChRTbmFwc2hvdExpc3RSZXNwb25zZRISCgpzZXNzaW9uX2lkGAEgASgJEigKCXNuYXBzaG90cxgCIAMoCzIVLm5ldGNsb2RlLnYxLlNuYXBzaG90EhcKCnJlcXVlc3RfaWQYAyABKAlIAIgBAUINCgtfcmVxdWVzdF9pZCKGAQoYU25hcHNob3RSZXN0b3JlZFJlc3BvbnNlEhIKCnNlc3Npb25faWQYASABKAkSEwoLc25hcHNob3RfaWQYAiABKAkSGQoRbWVzc2FnZXNfcmVzdG9yZWQYAyABKAUSFwoKcmVxdWVzdF9pZBgEIAEoCUgAiAEBQg0KC19yZXF1ZXN0X2lkIoUBChlSZXBvQWNjZXNzVXBkYXRlZFJlc3BvbnNlEhIKCnNlc3Npb25faWQYASABKAkSLAoLcmVwb19hY2Nlc3MYAiABKA4yFy5uZXRjbG9kZS52MS5SZXBvQWNjZXNzEhcKCnJlcXVlc3RfaWQYAyABKAlIAIgBAUINCgtfcmVxdWVzdF9pZCKcAQoWUmVzb3VyY2VMaW1pdHNSZXNwb25zZRIRCgltYXhfdmNwdXMYASABKAUSFQoNbWF4X21lbW9yeV9tYhgCIAEoBRIVCg1kZWZhdWx0X3ZjcHVzGAMgASgFEhkKEWRlZmF1bHRfbWVtb3J5X21iGAQgASgFEhcKCnJlcXVlc3RfaWQYBSABKAlIAIgBAUINCgtfcmVxdWVzdF9pZCquAQoOQ29kZXhBdXRoU3RhdGUSIAocQ09ERVhfQVVUSF9TVEFURV9VTlNQRUNJRklFRBAAEiQKIENPREVYX0FVVEhfU1RBVEVfVU5BVVRIRU5USUNBVEVEEAESHAoYQ09ERVhfQVVUSF9TVEFURV9QRU5ESU5HEAISGgoWQ09ERVhfQVVUSF9TVEFURV9SRUFEWRADEhoKFkNPREVYX0FVVEhfU1RBVEVfRVJST1IQBDJWCg1DbGllbnRTZXJ2aWNlEkUKB0Nvbm5lY3QSGi5uZXRjbG9kZS52MS5DbGllbnRNZXNzYWdlGhoubmV0Y2xvZGUudjEuU2VydmVyTWVzc2FnZSgBMAFCvAEKD2NvbS5uZXRjbG9kZS52MUILQ2xpZW50UHJvdG9QAVpPZ2l0aHViLmNvbS9hbmdyaXN0YW4vbmV0Y2xvZGUvc2VydmljZXMvY29udHJvbC1wbGFuZS9nZW4vbmV0Y2xvZGUvdjE7bmV0Y2xvZGV2MaICA05YWKoCC05ldGNsb2RlLlYxygILTmV0Y2xvZGVcVjHiAhdOZXRjbG9kZVxWMVxHUEJNZXRhZGF0YeoCDE5ldGNsb2RlOjpWMWIGcHJvdG8z", [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,26 @@ 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"; } | { case: undefined; value?: undefined }; }; @@ -315,6 +335,26 @@ 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"; } | { case: undefined; value?: undefined }; }; @@ -348,6 +388,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 +495,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 +509,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 +526,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 +562,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 +584,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 +606,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 +628,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 +645,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 +672,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 +694,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 +721,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 +753,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 +780,7 @@ 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.SyncRequest @@ -716,7 +797,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, 16); /** * @generated from message netclode.v1.ListGitHubReposRequest @@ -733,7 +814,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, 17); /** * @generated from message netclode.v1.GitStatusRequest @@ -755,7 +836,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, 18); /** * @generated from message netclode.v1.GitDiffRequest @@ -784,7 +865,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, 19); /** * @generated from message netclode.v1.ListModelsRequest @@ -808,6 +889,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 +903,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, 20); /** * @generated from message netclode.v1.GetCopilotStatusRequest @@ -832,7 +920,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, 21); /** * @generated from message netclode.v1.ListSnapshotsRequest @@ -854,7 +942,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, 22); /** * @generated from message netclode.v1.RestoreSnapshotRequest @@ -881,7 +969,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, 23); /** * @generated from message netclode.v1.UpdateRepoAccessRequest @@ -910,7 +998,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, 24); /** * @generated from message netclode.v1.GetResourceLimitsRequest @@ -927,7 +1015,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, 25); + +/** + * @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, 26); + +/** + * @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, 27); + +/** + * @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, 28); /** * @generated from message netclode.v1.SessionCreatedResponse @@ -951,7 +1090,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, 29); /** * @generated from message netclode.v1.SessionUpdatedResponse @@ -968,7 +1107,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, 30); /** * @generated from message netclode.v1.SessionDeletedResponse @@ -990,7 +1129,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, 31); /** * @generated from message netclode.v1.SessionsDeletedAllResponse @@ -1012,7 +1151,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, 32); /** * @generated from message netclode.v1.SessionListResponse @@ -1034,7 +1173,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, 33); /** * @generated from message netclode.v1.SessionStateResponse @@ -1084,7 +1223,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, 34); /** * @generated from message netclode.v1.SyncResponse @@ -1111,7 +1250,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, 35); /** * StreamEntryResponse wraps a StreamEntry for real-time push notifications. @@ -1136,7 +1275,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, 36); /** * @generated from message netclode.v1.PortExposedResponse @@ -1168,7 +1307,7 @@ 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, 37); /** * @generated from message netclode.v1.GitHubReposResponse @@ -1190,7 +1329,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, 38); /** * @generated from message netclode.v1.GitStatusResponse @@ -1217,7 +1356,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, 39); /** * @generated from message netclode.v1.GitDiffResponse @@ -1244,7 +1383,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, 40); /** * ErrorResponse is the unified error type for all error conditions. @@ -1273,7 +1412,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, 41); /** * @generated from message netclode.v1.ModelsResponse @@ -1304,7 +1443,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, 42); /** * @generated from message netclode.v1.CopilotStatusResponse @@ -1335,7 +1474,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, 43); + +/** + * @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, 44); + +/** + * @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, 45); + +/** + * @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, 46); /** * SnapshotCreatedResponse is pushed to clients when an auto-snapshot is created after a turn. @@ -1359,7 +1594,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, 47); /** * @generated from message netclode.v1.SnapshotListResponse @@ -1388,7 +1623,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, 48); /** * SnapshotRestoredResponse is sent after workspace and messages are restored. @@ -1424,7 +1659,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, 49); /** * RepoAccessUpdatedResponse is sent after repo access level is updated. @@ -1455,7 +1690,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, 50); /** * ResourceLimitsResponse contains the maximum sandbox resource allocation. @@ -1503,7 +1738,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, 51); + +/** + * @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/src/connect-client.ts b/services/agent/src/connect-client.ts index ad96ed87..32732ec2 100644 --- a/services/agent/src/connect-client.ts +++ b/services/agent/src/connect-client.ts @@ -483,7 +483,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 +543,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 +573,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 +637,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.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..00f1068d 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,6 +35,44 @@ 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; @@ -56,11 +94,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 +127,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 +177,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 +199,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"); 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/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..3378ed3f 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,9 @@ type ClientMessage struct { // *ClientMessage_RestoreSnapshot // *ClientMessage_UpdateRepoAccess // *ClientMessage_GetResourceLimits + // *ClientMessage_CodexAuthStart + // *ClientMessage_CodexAuthStatus + // *ClientMessage_CodexAuthLogout Message isClientMessage_Message `protobuf_oneof:"message"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache @@ -289,6 +347,33 @@ 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 +} + type isClientMessage_Message interface { isClientMessage_Message() } @@ -384,6 +469,19 @@ 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"` +} + func (*ClientMessage_CreateSession) isClientMessage_Message() {} func (*ClientMessage_ListSessions) isClientMessage_Message() {} @@ -428,6 +526,12 @@ 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() {} + // ServerMessage is the union of all server-to-client messages. type ServerMessage struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -453,6 +557,9 @@ type ServerMessage struct { // *ServerMessage_SnapshotRestored // *ServerMessage_RepoAccessUpdated // *ServerMessage_ResourceLimits + // *ServerMessage_CodexAuthStarted + // *ServerMessage_CodexAuthStatus + // *ServerMessage_CodexAuthLoggedOut Message isServerMessage_Message `protobuf_oneof:"message"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache @@ -675,6 +782,33 @@ 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 +} + type isServerMessage_Message interface { isServerMessage_Message() } @@ -764,6 +898,19 @@ 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"` +} + func (*ServerMessage_SessionCreated) isServerMessage_Message() {} func (*ServerMessage_SessionUpdated) isServerMessage_Message() {} @@ -804,6 +951,12 @@ 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() {} + // NetworkConfig controls network access for a session's sandbox. type NetworkConfig struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -852,25 +1005,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 +1105,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 +1118,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 +1191,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 +1207,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 +1219,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 +1232,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 +1254,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 +1266,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 +1279,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 +1320,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 +1332,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 +1345,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 +1372,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 +1384,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 +1397,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 +1424,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 +1436,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 +1449,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 +1475,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 +1487,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 +1500,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 +1521,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 +1533,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 +1546,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 +1580,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 +1592,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 +1605,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 +1633,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 +1645,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 +1658,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 +1694,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 +1706,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 +1719,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 +1761,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 +1773,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 +1786,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 { @@ -1589,7 +1819,7 @@ type SyncRequest struct { func (x *SyncRequest) Reset() { *x = SyncRequest{} - mi := &file_netclode_v1_client_proto_msgTypes[15] + mi := &file_netclode_v1_client_proto_msgTypes[16] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1601,7 +1831,7 @@ func (x *SyncRequest) String() string { func (*SyncRequest) ProtoMessage() {} func (x *SyncRequest) ProtoReflect() protoreflect.Message { - mi := &file_netclode_v1_client_proto_msgTypes[15] + mi := &file_netclode_v1_client_proto_msgTypes[16] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1614,7 +1844,7 @@ func (x *SyncRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use SyncRequest.ProtoReflect.Descriptor instead. func (*SyncRequest) Descriptor() ([]byte, []int) { - return file_netclode_v1_client_proto_rawDescGZIP(), []int{15} + return file_netclode_v1_client_proto_rawDescGZIP(), []int{16} } func (x *SyncRequest) GetRequestId() string { @@ -1633,7 +1863,7 @@ type ListGitHubReposRequest struct { func (x *ListGitHubReposRequest) Reset() { *x = ListGitHubReposRequest{} - mi := &file_netclode_v1_client_proto_msgTypes[16] + mi := &file_netclode_v1_client_proto_msgTypes[17] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1645,7 +1875,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[17] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1658,7 +1888,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{17} } func (x *ListGitHubReposRequest) GetRequestId() string { @@ -1678,7 +1908,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[18] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1690,7 +1920,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[18] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1703,7 +1933,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{18} } func (x *GitStatusRequest) GetRequestId() string { @@ -1731,7 +1961,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[19] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1743,7 +1973,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[19] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1756,7 +1986,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{19} } func (x *GitDiffRequest) GetRequestId() string { @@ -1781,17 +2011,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[20] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1803,7 +2034,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[20] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1816,7 +2047,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{20} } func (x *ListModelsRequest) GetRequestId() string { @@ -1840,6 +2071,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 +2087,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[21] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1861,7 +2099,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[21] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1874,7 +2112,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{21} } func (x *GetCopilotStatusRequest) GetRequestId() string { @@ -1894,7 +2132,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[22] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1906,7 +2144,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[22] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1919,7 +2157,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{22} } func (x *ListSnapshotsRequest) GetRequestId() string { @@ -1947,7 +2185,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[23] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1959,7 +2197,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[23] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1972,7 +2210,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{23} } func (x *RestoreSnapshotRequest) GetRequestId() string { @@ -2007,7 +2245,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[24] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2019,7 +2257,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[24] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2032,7 +2270,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{24} } func (x *UpdateRepoAccessRequest) GetRequestId() string { @@ -2065,7 +2303,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[25] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2077,7 +2315,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[25] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2090,7 +2328,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{25} } func (x *GetResourceLimitsRequest) GetRequestId() string { @@ -2100,6 +2338,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[26] + 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[26] + 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{26} +} + +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[27] + 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[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 CodexAuthStatusRequest.ProtoReflect.Descriptor instead. +func (*CodexAuthStatusRequest) Descriptor() ([]byte, []int) { + return file_netclode_v1_client_proto_rawDescGZIP(), []int{27} +} + +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[28] + 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[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 CodexAuthLogoutRequest.ProtoReflect.Descriptor instead. +func (*CodexAuthLogoutRequest) Descriptor() ([]byte, []int) { + return file_netclode_v1_client_proto_rawDescGZIP(), []int{28} +} + +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 +2480,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[29] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2122,7 +2492,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[29] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2135,7 +2505,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{29} } func (x *SessionCreatedResponse) GetSession() *Session { @@ -2161,7 +2531,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[30] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2173,7 +2543,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[30] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2186,7 +2556,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{30} } func (x *SessionUpdatedResponse) GetSession() *Session { @@ -2206,7 +2576,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[31] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2218,7 +2588,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[31] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2231,7 +2601,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{31} } func (x *SessionDeletedResponse) GetSessionId() string { @@ -2258,7 +2628,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[32] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2270,7 +2640,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[32] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2283,7 +2653,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{32} } func (x *SessionsDeletedAllResponse) GetDeletedIds() []string { @@ -2310,7 +2680,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[33] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2322,7 +2692,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[33] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2335,7 +2705,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{33} } func (x *SessionListResponse) GetSessions() []*Session { @@ -2366,7 +2736,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[34] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2378,7 +2748,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[34] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2391,7 +2761,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{34} } func (x *SessionStateResponse) GetSession() *Session { @@ -2447,7 +2817,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[35] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2459,7 +2829,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[35] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2472,7 +2842,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{35} } func (x *SyncResponse) GetSessions() []*SessionSummary { @@ -2508,7 +2878,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[36] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2520,7 +2890,7 @@ 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[36] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2533,7 +2903,7 @@ func (x *StreamEntryResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use StreamEntryResponse.ProtoReflect.Descriptor instead. func (*StreamEntryResponse) Descriptor() ([]byte, []int) { - return file_netclode_v1_client_proto_rawDescGZIP(), []int{32} + return file_netclode_v1_client_proto_rawDescGZIP(), []int{36} } func (x *StreamEntryResponse) GetSessionId() string { @@ -2562,7 +2932,7 @@ type PortExposedResponse struct { func (x *PortExposedResponse) Reset() { *x = PortExposedResponse{} - mi := &file_netclode_v1_client_proto_msgTypes[33] + mi := &file_netclode_v1_client_proto_msgTypes[37] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2574,7 +2944,7 @@ func (x *PortExposedResponse) String() string { func (*PortExposedResponse) ProtoMessage() {} func (x *PortExposedResponse) ProtoReflect() protoreflect.Message { - mi := &file_netclode_v1_client_proto_msgTypes[33] + mi := &file_netclode_v1_client_proto_msgTypes[37] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2587,7 +2957,7 @@ func (x *PortExposedResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use PortExposedResponse.ProtoReflect.Descriptor instead. func (*PortExposedResponse) Descriptor() ([]byte, []int) { - return file_netclode_v1_client_proto_rawDescGZIP(), []int{33} + return file_netclode_v1_client_proto_rawDescGZIP(), []int{37} } func (x *PortExposedResponse) GetSessionId() string { @@ -2628,7 +2998,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[38] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2640,7 +3010,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[38] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2653,7 +3023,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{38} } func (x *GitHubReposResponse) GetRepos() []*GitHubRepo { @@ -2681,7 +3051,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[39] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2693,7 +3063,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[39] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2706,7 +3076,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{39} } func (x *GitStatusResponse) GetSessionId() string { @@ -2741,7 +3111,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[40] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2753,7 +3123,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[40] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2766,7 +3136,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{40} } func (x *GitDiffResponse) GetSessionId() string { @@ -2802,7 +3172,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[41] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2814,7 +3184,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[41] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2827,7 +3197,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{41} } func (x *ErrorResponse) GetError() *Error { @@ -2855,7 +3225,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[42] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2867,7 +3237,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[42] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2880,7 +3250,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{42} } func (x *ModelsResponse) GetModels() []*ModelInfo { @@ -2915,7 +3285,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[43] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2927,7 +3297,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[43] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2940,7 +3310,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{43} } func (x *CopilotStatusResponse) GetAuth() *CopilotAuthStatus { @@ -2964,6 +3334,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[44] + 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[44] + 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{44} +} + +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[45] + 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[45] + 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{45} +} + +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[46] + 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[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 CodexAuthLoggedOutResponse.ProtoReflect.Descriptor instead. +func (*CodexAuthLoggedOutResponse) Descriptor() ([]byte, []int) { + return file_netclode_v1_client_proto_rawDescGZIP(), []int{46} +} + +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 +3549,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[47] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2987,7 +3561,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[47] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3000,7 +3574,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{47} } func (x *SnapshotCreatedResponse) GetSessionId() string { @@ -3028,7 +3602,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[48] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3040,7 +3614,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[48] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3053,7 +3627,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{48} } func (x *SnapshotListResponse) GetSessionId() string { @@ -3090,7 +3664,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[49] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3102,7 +3676,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[49] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3115,7 +3689,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{49} } func (x *SnapshotRestoredResponse) GetSessionId() string { @@ -3158,7 +3732,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[50] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3170,7 +3744,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[50] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3183,7 +3757,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{50} } func (x *RepoAccessUpdatedResponse) GetSessionId() string { @@ -3222,7 +3796,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[51] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3234,7 +3808,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[51] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3247,7 +3821,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{51} } func (x *ResourceLimitsResponse) GetMaxVcpus() int32 { @@ -3289,7 +3863,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\"\xfb\x0e\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 +3891,11 @@ 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\x0fcodexAuthLogoutB\t\n" + + "\amessage\"\xe9\r\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 +3917,20 @@ 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\x12codexAuthLoggedOutB\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 +3944,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 +3955,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" + @@ -3460,14 +4049,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 +4088,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" + @@ -3591,6 +4194,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 +4258,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 +4281,167 @@ 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, 52) 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 + (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 + (*SyncRequest)(nil), // 17: netclode.v1.SyncRequest + (*ListGitHubReposRequest)(nil), // 18: netclode.v1.ListGitHubReposRequest + (*GitStatusRequest)(nil), // 19: netclode.v1.GitStatusRequest + (*GitDiffRequest)(nil), // 20: netclode.v1.GitDiffRequest + (*ListModelsRequest)(nil), // 21: netclode.v1.ListModelsRequest + (*GetCopilotStatusRequest)(nil), // 22: netclode.v1.GetCopilotStatusRequest + (*ListSnapshotsRequest)(nil), // 23: netclode.v1.ListSnapshotsRequest + (*RestoreSnapshotRequest)(nil), // 24: netclode.v1.RestoreSnapshotRequest + (*UpdateRepoAccessRequest)(nil), // 25: netclode.v1.UpdateRepoAccessRequest + (*GetResourceLimitsRequest)(nil), // 26: netclode.v1.GetResourceLimitsRequest + (*CodexAuthStartRequest)(nil), // 27: netclode.v1.CodexAuthStartRequest + (*CodexAuthStatusRequest)(nil), // 28: netclode.v1.CodexAuthStatusRequest + (*CodexAuthLogoutRequest)(nil), // 29: netclode.v1.CodexAuthLogoutRequest + (*SessionCreatedResponse)(nil), // 30: netclode.v1.SessionCreatedResponse + (*SessionUpdatedResponse)(nil), // 31: netclode.v1.SessionUpdatedResponse + (*SessionDeletedResponse)(nil), // 32: netclode.v1.SessionDeletedResponse + (*SessionsDeletedAllResponse)(nil), // 33: netclode.v1.SessionsDeletedAllResponse + (*SessionListResponse)(nil), // 34: netclode.v1.SessionListResponse + (*SessionStateResponse)(nil), // 35: netclode.v1.SessionStateResponse + (*SyncResponse)(nil), // 36: netclode.v1.SyncResponse + (*StreamEntryResponse)(nil), // 37: netclode.v1.StreamEntryResponse + (*PortExposedResponse)(nil), // 38: netclode.v1.PortExposedResponse + (*GitHubReposResponse)(nil), // 39: netclode.v1.GitHubReposResponse + (*GitStatusResponse)(nil), // 40: netclode.v1.GitStatusResponse + (*GitDiffResponse)(nil), // 41: netclode.v1.GitDiffResponse + (*ErrorResponse)(nil), // 42: netclode.v1.ErrorResponse + (*ModelsResponse)(nil), // 43: netclode.v1.ModelsResponse + (*CopilotStatusResponse)(nil), // 44: netclode.v1.CopilotStatusResponse + (*CodexAuthStartedResponse)(nil), // 45: netclode.v1.CodexAuthStartedResponse + (*CodexAuthStatusResponse)(nil), // 46: netclode.v1.CodexAuthStatusResponse + (*CodexAuthLoggedOutResponse)(nil), // 47: netclode.v1.CodexAuthLoggedOutResponse + (*SnapshotCreatedResponse)(nil), // 48: netclode.v1.SnapshotCreatedResponse + (*SnapshotListResponse)(nil), // 49: netclode.v1.SnapshotListResponse + (*SnapshotRestoredResponse)(nil), // 50: netclode.v1.SnapshotRestoredResponse + (*RepoAccessUpdatedResponse)(nil), // 51: netclode.v1.RepoAccessUpdatedResponse + (*ResourceLimitsResponse)(nil), // 52: netclode.v1.ResourceLimitsResponse (*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 + (RepoAccess)(0), // 54: netclode.v1.RepoAccess + (SdkType)(0), // 55: netclode.v1.SdkType + (CopilotBackend)(0), // 56: netclode.v1.CopilotBackend + (*SandboxResources)(nil), // 57: netclode.v1.SandboxResources + (*Session)(nil), // 58: netclode.v1.Session + (*StreamEntry)(nil), // 59: netclode.v1.StreamEntry + (*InProgressState)(nil), // 60: netclode.v1.InProgressState + (*SessionSummary)(nil), // 61: netclode.v1.SessionSummary + (*GitHubRepo)(nil), // 62: netclode.v1.GitHubRepo + (*GitFileChange)(nil), // 63: netclode.v1.GitFileChange + (*Error)(nil), // 64: netclode.v1.Error + (*ModelInfo)(nil), // 65: netclode.v1.ModelInfo + (*CopilotAuthStatus)(nil), // 66: netclode.v1.CopilotAuthStatus + (*CopilotPremiumQuota)(nil), // 67: netclode.v1.CopilotPremiumQuota + (*Snapshot)(nil), // 68: 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 + 17, // 12: netclode.v1.ClientMessage.sync:type_name -> netclode.v1.SyncRequest + 18, // 13: netclode.v1.ClientMessage.list_github_repos:type_name -> netclode.v1.ListGitHubReposRequest + 19, // 14: netclode.v1.ClientMessage.git_status:type_name -> netclode.v1.GitStatusRequest + 20, // 15: netclode.v1.ClientMessage.git_diff:type_name -> netclode.v1.GitDiffRequest + 21, // 16: netclode.v1.ClientMessage.list_models:type_name -> netclode.v1.ListModelsRequest + 22, // 17: netclode.v1.ClientMessage.get_copilot_status:type_name -> netclode.v1.GetCopilotStatusRequest + 23, // 18: netclode.v1.ClientMessage.list_snapshots:type_name -> netclode.v1.ListSnapshotsRequest + 24, // 19: netclode.v1.ClientMessage.restore_snapshot:type_name -> netclode.v1.RestoreSnapshotRequest + 25, // 20: netclode.v1.ClientMessage.update_repo_access:type_name -> netclode.v1.UpdateRepoAccessRequest + 26, // 21: netclode.v1.ClientMessage.get_resource_limits:type_name -> netclode.v1.GetResourceLimitsRequest + 27, // 22: netclode.v1.ClientMessage.codex_auth_start:type_name -> netclode.v1.CodexAuthStartRequest + 28, // 23: netclode.v1.ClientMessage.codex_auth_status:type_name -> netclode.v1.CodexAuthStatusRequest + 29, // 24: netclode.v1.ClientMessage.codex_auth_logout:type_name -> netclode.v1.CodexAuthLogoutRequest + 30, // 25: netclode.v1.ServerMessage.session_created:type_name -> netclode.v1.SessionCreatedResponse + 31, // 26: netclode.v1.ServerMessage.session_updated:type_name -> netclode.v1.SessionUpdatedResponse + 32, // 27: netclode.v1.ServerMessage.session_deleted:type_name -> netclode.v1.SessionDeletedResponse + 33, // 28: netclode.v1.ServerMessage.sessions_deleted_all:type_name -> netclode.v1.SessionsDeletedAllResponse + 34, // 29: netclode.v1.ServerMessage.session_list:type_name -> netclode.v1.SessionListResponse + 35, // 30: netclode.v1.ServerMessage.session_state:type_name -> netclode.v1.SessionStateResponse + 36, // 31: netclode.v1.ServerMessage.sync_response:type_name -> netclode.v1.SyncResponse + 37, // 32: netclode.v1.ServerMessage.stream_entry:type_name -> netclode.v1.StreamEntryResponse + 38, // 33: netclode.v1.ServerMessage.port_exposed:type_name -> netclode.v1.PortExposedResponse + 39, // 34: netclode.v1.ServerMessage.github_repos:type_name -> netclode.v1.GitHubReposResponse + 40, // 35: netclode.v1.ServerMessage.git_status:type_name -> netclode.v1.GitStatusResponse + 41, // 36: netclode.v1.ServerMessage.git_diff:type_name -> netclode.v1.GitDiffResponse + 42, // 37: netclode.v1.ServerMessage.error:type_name -> netclode.v1.ErrorResponse + 43, // 38: netclode.v1.ServerMessage.models:type_name -> netclode.v1.ModelsResponse + 44, // 39: netclode.v1.ServerMessage.copilot_status:type_name -> netclode.v1.CopilotStatusResponse + 48, // 40: netclode.v1.ServerMessage.snapshot_created:type_name -> netclode.v1.SnapshotCreatedResponse + 49, // 41: netclode.v1.ServerMessage.snapshot_list:type_name -> netclode.v1.SnapshotListResponse + 50, // 42: netclode.v1.ServerMessage.snapshot_restored:type_name -> netclode.v1.SnapshotRestoredResponse + 51, // 43: netclode.v1.ServerMessage.repo_access_updated:type_name -> netclode.v1.RepoAccessUpdatedResponse + 52, // 44: netclode.v1.ServerMessage.resource_limits:type_name -> netclode.v1.ResourceLimitsResponse + 45, // 45: netclode.v1.ServerMessage.codex_auth_started:type_name -> netclode.v1.CodexAuthStartedResponse + 46, // 46: netclode.v1.ServerMessage.codex_auth_status:type_name -> netclode.v1.CodexAuthStatusResponse + 47, // 47: netclode.v1.ServerMessage.codex_auth_logged_out:type_name -> netclode.v1.CodexAuthLoggedOutResponse + 53, // 48: netclode.v1.CodexOAuthTokens.expires_at:type_name -> google.protobuf.Timestamp + 54, // 49: netclode.v1.CreateSessionRequest.repo_access:type_name -> netclode.v1.RepoAccess + 55, // 50: netclode.v1.CreateSessionRequest.sdk_type:type_name -> netclode.v1.SdkType + 56, // 51: netclode.v1.CreateSessionRequest.copilot_backend:type_name -> netclode.v1.CopilotBackend + 3, // 52: netclode.v1.CreateSessionRequest.network_config:type_name -> netclode.v1.NetworkConfig + 57, // 53: netclode.v1.CreateSessionRequest.resources:type_name -> netclode.v1.SandboxResources + 4, // 54: netclode.v1.CreateSessionRequest.codex_oauth_tokens:type_name -> netclode.v1.CodexOAuthTokens + 55, // 55: netclode.v1.ListModelsRequest.sdk_type:type_name -> netclode.v1.SdkType + 56, // 56: netclode.v1.ListModelsRequest.copilot_backend:type_name -> netclode.v1.CopilotBackend + 54, // 57: netclode.v1.UpdateRepoAccessRequest.repo_access:type_name -> netclode.v1.RepoAccess + 58, // 58: netclode.v1.SessionCreatedResponse.session:type_name -> netclode.v1.Session + 58, // 59: netclode.v1.SessionUpdatedResponse.session:type_name -> netclode.v1.Session + 58, // 60: netclode.v1.SessionListResponse.sessions:type_name -> netclode.v1.Session + 58, // 61: netclode.v1.SessionStateResponse.session:type_name -> netclode.v1.Session + 59, // 62: netclode.v1.SessionStateResponse.entries:type_name -> netclode.v1.StreamEntry + 60, // 63: netclode.v1.SessionStateResponse.in_progress:type_name -> netclode.v1.InProgressState + 61, // 64: netclode.v1.SyncResponse.sessions:type_name -> netclode.v1.SessionSummary + 53, // 65: netclode.v1.SyncResponse.server_time:type_name -> google.protobuf.Timestamp + 59, // 66: netclode.v1.StreamEntryResponse.entry:type_name -> netclode.v1.StreamEntry + 62, // 67: netclode.v1.GitHubReposResponse.repos:type_name -> netclode.v1.GitHubRepo + 63, // 68: netclode.v1.GitStatusResponse.files:type_name -> netclode.v1.GitFileChange + 64, // 69: netclode.v1.ErrorResponse.error:type_name -> netclode.v1.Error + 65, // 70: netclode.v1.ModelsResponse.models:type_name -> netclode.v1.ModelInfo + 55, // 71: netclode.v1.ModelsResponse.sdk_type:type_name -> netclode.v1.SdkType + 66, // 72: netclode.v1.CopilotStatusResponse.auth:type_name -> netclode.v1.CopilotAuthStatus + 67, // 73: netclode.v1.CopilotStatusResponse.quota:type_name -> netclode.v1.CopilotPremiumQuota + 53, // 74: netclode.v1.CodexAuthStartedResponse.expires_at:type_name -> google.protobuf.Timestamp + 0, // 75: netclode.v1.CodexAuthStatusResponse.state:type_name -> netclode.v1.CodexAuthState + 53, // 76: netclode.v1.CodexAuthStatusResponse.expires_at:type_name -> google.protobuf.Timestamp + 68, // 77: netclode.v1.SnapshotCreatedResponse.snapshot:type_name -> netclode.v1.Snapshot + 68, // 78: netclode.v1.SnapshotListResponse.snapshots:type_name -> netclode.v1.Snapshot + 54, // 79: netclode.v1.RepoAccessUpdatedResponse.repo_access:type_name -> netclode.v1.RepoAccess + 1, // 80: netclode.v1.ClientService.Connect:input_type -> netclode.v1.ClientMessage + 2, // 81: netclode.v1.ClientService.Connect:output_type -> netclode.v1.ServerMessage + 81, // [81:82] is the sub-list for method output_type + 80, // [80:81] is the sub-list for method input_type + 80, // [80:80] is the sub-list for extension type_name + 80, // [80:80] is the sub-list for extension extendee + 0, // [0:80] is the sub-list for field type_name } func init() { file_netclode_v1_client_proto_init() } @@ -3817,6 +4474,9 @@ 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), } file_netclode_v1_client_proto_msgTypes[1].OneofWrappers = []any{ (*ServerMessage_SessionCreated)(nil), @@ -3839,6 +4499,9 @@ 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), } file_netclode_v1_client_proto_msgTypes[3].OneofWrappers = []any{} file_netclode_v1_client_proto_msgTypes[4].OneofWrappers = []any{} @@ -3863,34 +4526,42 @@ 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[48].OneofWrappers = []any{} + file_netclode_v1_client_proto_msgTypes[49].OneofWrappers = []any{} + file_netclode_v1_client_proto_msgTypes[50].OneofWrappers = []any{} + file_netclode_v1_client_proto_msgTypes[51].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: 52, 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/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..75acdff6 100644 --- a/services/control-plane/internal/api/connect_client.go +++ b/services/control-plane/internal/api/connect_client.go @@ -211,6 +211,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 +377,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 { @@ -731,6 +745,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 +856,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/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/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..24ccce43 100644 --- a/services/control-plane/internal/session/manager.go +++ b/services/control-plane/internal/session/manager.go @@ -30,18 +30,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 +58,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 +82,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 +279,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, "") @@ -1522,10 +1547,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 +1585,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 +2130,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 +2160,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 +2753,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 { diff --git a/services/control-plane/internal/session/manager_test.go b/services/control-plane/internal/session/manager_test.go index df00ada7..92e7d853 100644 --- a/services/control-plane/internal/session/manager_test.go +++ b/services/control-plane/internal/session/manager_test.go @@ -2,6 +2,10 @@ package session import ( "context" + "encoding/json" + "net/http" + "net/http/httptest" + "strings" "sync" "testing" "time" @@ -247,13 +251,16 @@ 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 + oauth map[string]*storage.CodexOAuthSessionData + oauthGlobal *storage.CodexOAuthSessionData } func newMockStorage() *mockStorage { return &mockStorage{ sessions: make(map[string]*pb.Session), + oauth: make(map[string]*storage.CodexOAuthSessionData), } } @@ -296,10 +303,70 @@ 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 } @@ -586,10 +653,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 +697,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 +855,199 @@ 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) + } +} 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) From cf9f1f7d770c67487c5c0dca0fed24532f1375fc Mon Sep 17 00:00:00 2001 From: Bazyli Brzoska Date: Fri, 6 Feb 2026 21:41:23 -0800 Subject: [PATCH 07/21] fix(ios): surface new-session create failures instead of silent dismissal Creating a new session could fail on the backend (for example Codex :oauth not configured), but the UI provided no visible feedback. Before this change: - Prompt sheet sent create request and dismissed immediately - Generic/server errors were only printed to console - Users saw "nothing happened" with no reason This change adds an explicit create-failure feedback path: - Introduce `pendingCreationError` in `SessionStore` for errors that occur before a session ID exists - Route `sessionError` (without session ID) and generic `.error` into that state when a session creation is pending - Clear stale create errors on successful `sessionCreated` and at the next submit - Show a `Session Creation Failed` alert in `SessionsView` with the server-provided message - Trigger error haptic when the alert state is set Result: - New-session failures are visible and actionable - The app no longer fails silently when create is rejected by backend policy/config Validation: - Built Mac Catalyst app successfully: xcodebuild -scheme Netclode -destination 'platform=macOS,variant=Mac Catalyst' -derivedDataPath .build CODE_SIGNING_ALLOWED=NO build --- .../Features/Sessions/PromptSheet.swift | 1 + .../Features/Sessions/SessionsView.swift | 17 +++++++++++++++++ .../ios/Netclode/Services/MessageRouter.swift | 6 ++++++ clients/ios/Netclode/Stores/SessionStore.swift | 15 +++++++++++++++ 4 files changed, 39 insertions(+) diff --git a/clients/ios/Netclode/Features/Sessions/PromptSheet.swift b/clients/ios/Netclode/Features/Sessions/PromptSheet.swift index 6ed33d4e..15aa0757 100644 --- a/clients/ios/Netclode/Features/Sessions/PromptSheet.swift +++ b/clients/ios/Netclode/Features/Sessions/PromptSheet.swift @@ -550,6 +550,7 @@ struct PromptSheet: View { guard !text.isEmpty else { return } isSubmitting = true + sessionStore.clearPendingCreationError() if settingsStore.hapticFeedbackEnabled { HapticFeedback.medium() 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/Services/MessageRouter.swift b/clients/ios/Netclode/Services/MessageRouter.swift index 127eae7f..7b4cdaea 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 @@ -271,6 +274,9 @@ final class MessageRouter { // 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) { From 08af6acc6937ade4b20d60fb43fa76765d8bfc47 Mon Sep 17 00:00:00 2001 From: Bazyli Brzoska Date: Fri, 6 Feb 2026 21:59:31 -0800 Subject: [PATCH 08/21] fix(agent,control-plane): make interrupt paths terminate prompt lifecycle Root cause - Codex adapter only toggled an interrupt flag, but did not abort in-flight runStreamed() calls. - If runStreamed() blocked before yielding events, the interrupt never surfaced as a terminal event. - Control-plane ignored AgentSystemMessage("interrupted") for lifecycle transitions, so sessions could remain RUNNING indefinitely without result/error. What changed - services/agent/src/sdk/codex/adapter.ts - Added AbortController state to CodexAdapter. - Passed turnOptions.signal into thread.runStreamed(...). - Updated setInterruptSignal() to abort active run immediately. - Treated abort/interrupt as a terminal PromptEvent error (retryable=true). - Cleared abort controller on completion and shutdown. - services/control-plane/internal/session/agent_handlers.go - Added handleAgentSystemMessage(). - Interprets system message "interrupted" as terminal for prompt lifecycle. - Emits agent done, sets session READY, updates last_active_at, and clears streaming state. - services/control-plane/internal/session/manager_test.go - Added tests asserting interrupted system messages transition RUNNING -> READY and reset streaming buffers. - Added non-interrupt system message regression test to ensure no status change. - services/agent/src/sdk/codex/adapter.interrupt.test.ts - Added regression test asserting AbortSignal is passed to runStreamed and interrupt emits terminal retryable error. Why this prevents recurrence - Interrupt now has a hard cancellation path at the SDK call boundary, not just inside event iteration. - Control-plane now recognizes interrupt as a terminal condition even if no result/error frame arrives. - Together these changes prevent stuck RUNNING sessions caused by orphaned in-flight prompt execution. Validation - services/agent: npm test -- src/sdk/codex/adapter.interrupt.test.ts src/sdk/codex/adapter.test.ts - services/agent: npm run typecheck - services/control-plane: go test ./internal/session -count=1 - Manual verification against affected session: prompt execution resumed successfully after recovery. --- .../src/sdk/codex/adapter.interrupt.test.ts | 92 +++++++++++++++++++ services/agent/src/sdk/codex/adapter.ts | 41 ++++++++- .../internal/session/agent_handlers.go | 32 ++++++- .../internal/session/manager_test.go | 52 +++++++++++ 4 files changed, 212 insertions(+), 5 deletions(-) create mode 100644 services/agent/src/sdk/codex/adapter.interrupt.test.ts 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.ts b/services/agent/src/sdk/codex/adapter.ts index 00f1068d..fcf80b27 100644 --- a/services/agent/src/sdk/codex/adapter.ts +++ b/services/agent/src/sdk/codex/adapter.ts @@ -78,6 +78,7 @@ export class CodexAdapter implements SDKAdapter { 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) @@ -243,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); @@ -281,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; } @@ -316,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); } @@ -341,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/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/manager_test.go b/services/control-plane/internal/session/manager_test.go index 92e7d853..2c56582f 100644 --- a/services/control-plane/internal/session/manager_test.go +++ b/services/control-plane/internal/session/manager_test.go @@ -1051,3 +1051,55 @@ func TestGetAllowedSecretForHost_CodexHasNoProxySecret(t *testing.T) { 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) + } +} From 24a06d811d9dfe500bd9efe3a40ddb57d72d41a7 Mon Sep 17 00:00:00 2001 From: Bazyli Brzoska Date: Fri, 6 Feb 2026 22:28:47 -0800 Subject: [PATCH 09/21] feat(ios): add cmd+enter send shortcut in chat input Root cause - The in-session chat composer had no hardware keyboard shortcut for submit, so iPad/Mac keyboard users could not send via Cmd+Enter. - Send action availability depended on UI mode, but shortcut behavior was not explicitly constrained to send mode. What changed - Updated `clients/ios/Netclode/Features/Chat/ChatInputBar.swift` send button to register `.keyboardShortcut(.return, modifiers: .command)`. - Tightened send button disable logic from `!canSend` to `!canSend || rightButtonMode != .send` so the shortcut only fires when send mode is active. Why this works - The shortcut is attached to the same `Button(action: onSend)` used for tap send, so it reuses existing send flow and state handling. - Additional disable gating prevents accidental activation while mic/interrupt/loading modes are active. Validation - Manually validated --- clients/ios/Netclode/Features/Chat/ChatInputBar.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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) From 39018198fce8c337e0b9ca699415830c75a0512b Mon Sep 17 00:00:00 2001 From: Bazyli Brzoska Date: Fri, 6 Feb 2026 23:36:53 -0800 Subject: [PATCH 10/21] fix(ios): prevent expose port sheet env crash Root cause - `ExposePortSheet` declared `@Environment(SettingsStore.self)` but did not use it. - When the sheet was presented from the previews flow, SwiftUI attempted to resolve the missing environment value and hit an assertion on macOS. What changed - Removed the unused `SettingsStore` environment dependency from `ExposePortSheet` in `clients/ios/Netclode/Features/Chat/ChatView.swift`. Why this works - The sheet now depends only on values it actually uses (`dismiss` + local state), so presentation no longer requires an unnecessary environment injection path. Validation - Code path inspected against crash trace (`EnvironmentValues.subscript.getter` during sheet presentation). - Full iOS/macOS build was not run in this step due long Swift package checkout overhead in this environment. --- clients/ios/Netclode/Features/Chat/ChatView.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/clients/ios/Netclode/Features/Chat/ChatView.swift b/clients/ios/Netclode/Features/Chat/ChatView.swift index 6abe6380..e660fc06 100644 --- a/clients/ios/Netclode/Features/Chat/ChatView.swift +++ b/clients/ios/Netclode/Features/Chat/ChatView.swift @@ -741,7 +741,6 @@ struct ExposePortSheet: View { let onExpose: (Int) -> Void @Environment(\.dismiss) private var dismiss - @Environment(SettingsStore.self) private var settingsStore @FocusState private var isInputFocused: Bool From 7bc9b2a14cecd281302ea358815981fc9907a095 Mon Sep 17 00:00:00 2001 From: Bazyli Brzoska Date: Fri, 6 Feb 2026 23:37:07 -0800 Subject: [PATCH 11/21] fix(control-plane): return routable preview URLs and tighten ingress RBAC Root cause - Exposed-port preview URLs were emitted with a short hostname (`sandbox-:`), which may not resolve on client devices. What changed - Added preview hostname resolution in control-plane runtime: - Read Tailscale service hostname for `ts-`. - If short, infer tailnet suffix from `Ingress/control-plane` hostname. - Build `preview_url` using the resolved host, with short-host fallback on lookup failure. - Added/updated tests for resolved-host and fallback behavior in session manager tests. - Used tight control-plane Role permissions for ingress access: - `resources: ["ingresses"]` - `resourceNames: ["control-plane"]` - `verbs: ["get"]` Why this works - Clients now receive a routable MagicDNS-style preview URL by default. - Least-privilege RBAC still allows the exact lookup path required by code. - Fallback behavior preserves resilience if metadata is temporarily unavailable. Validation - `cd services/control-plane && go test ./...` - Deployed manifests to the target environment with Ansible (`playbooks/site.yaml --tags k8s-manifests`, `ansible_user=ubuntu`). - Verified RBAC as control-plane service account: - can get `ingress/control-plane` - cannot get other ingress names - cannot list/watch ingresses - Smoke-tested `port expose` end-to-end: returned an FQDN-style preview URL. --- infra/k8s/namespace.yaml | 5 ++ services/control-plane/internal/k8s/client.go | 1 + .../control-plane/internal/k8s/sandbox.go | 46 +++++++++++++++++++ .../control-plane/internal/session/manager.go | 7 ++- .../internal/session/manager_test.go | 40 ++++++++++++++++ 5 files changed, 98 insertions(+), 1 deletion(-) 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/services/control-plane/internal/k8s/client.go b/services/control-plane/internal/k8s/client.go index 466a59ec..7ab1d78a 100644 --- a/services/control-plane/internal/k8s/client.go +++ b/services/control-plane/internal/k8s/client.go @@ -42,6 +42,7 @@ 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 + GetSandboxPreviewHostname(ctx context.Context, sessionID string) (string, error) // Network policy operations ConfigureNetwork(ctx context.Context, sessionID string, networkEnabled bool) error diff --git a/services/control-plane/internal/k8s/sandbox.go b/services/control-plane/internal/k8s/sandbox.go index a8834b1d..e6d1dec6 100644 --- a/services/control-plane/internal/k8s/sandbox.go +++ b/services/control-plane/internal/k8s/sandbox.go @@ -56,6 +56,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 { @@ -973,6 +975,50 @@ func (r *k8sRuntime) ExposePort(ctx context.Context, sessionID string, port int) 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). diff --git a/services/control-plane/internal/session/manager.go b/services/control-plane/internal/session/manager.go index 24ccce43..0bdeea68 100644 --- a/services/control-plane/internal/session/manager.go +++ b/services/control-plane/internal/session/manager.go @@ -1094,7 +1094,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 diff --git a/services/control-plane/internal/session/manager_test.go b/services/control-plane/internal/session/manager_test.go index 2c56582f..aa7400a0 100644 --- a/services/control-plane/internal/session/manager_test.go +++ b/services/control-plane/internal/session/manager_test.go @@ -3,6 +3,7 @@ package session import ( "context" "encoding/json" + "errors" "net/http" "net/http/httptest" "strings" @@ -31,6 +32,8 @@ type mockRuntime struct { createdServices []string labeledSandboxes map[string]string // sandboxName -> sessionID readyCallbacks map[string][]k8s.SandboxReadyCallback + previewHostname string + previewHostErr error } func newMockRuntime() *mockRuntime { @@ -137,6 +140,13 @@ func (m *mockRuntime) ExposePort(ctx context.Context, sessionID string, port int 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() @@ -1103,3 +1113,33 @@ func TestHandleAgentResponse_SystemMessageNonInterruptNoStatusChange(t *testing. 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) + } +} From 46656ab0566af55da7c79087126f669935457bb4 Mon Sep 17 00:00:00 2001 From: Bazyli Brzoska Date: Sat, 7 Feb 2026 00:14:30 -0800 Subject: [PATCH 12/21] feat(ports): add end-to-end port unexpose support Root cause - Port management only supported expose operations, so users could not remove a previously exposed port from the UI or API. - Resume restoration logic only replayed expose events, so there was no persisted state transition representing port removal. What changed - Extended protocol definitions to include unexpose request/response and a dedicated port_unexposed agent event. - Regenerated protobuf outputs for control-plane (Go), iOS (Swift), and agent/client TS bindings. - Added control-plane API handling for unexpose messages and response emission. - Implemented session manager UnexposePort behavior, persisted port_unexposed events, and updated restore logic to derive final state from expose/unexpose event history. - Implemented Kubernetes runtime UnexposePort to remove the port from both the Tailscale Service and session NetworkPolicy ingress rules. - Added CLI command `port unexpose` and event rendering for port_unexposed in `events` output. - Added iOS message/event plumbing for unexpose, including persisted event decoding, stream conversion, and router handling. - Updated iOS Previews UI to compute active ports from event history and added a remove action for each exposed port. - Added iOS chat rendering for port removal events. - Extended session tests with stream-backed mock storage and new coverage for unexpose persistence and restore-state correctness. - Switched protobuf event decoding in restore/snapshot counting paths to protojson to correctly handle oneof payloads. Why this works - Port removal now has an explicit protocol and runtime path, so clients can request unexpose and receive deterministic acknowledgment/events. - Persisting port_unexposed events makes exposed-port state reconstructable over pause/resume and reconnects. - Applying expose/unexpose events in order ensures restoration reflects the latest intended state instead of stale exposures. - Removing ingress/service entries at the runtime layer ensures network access is actually revoked, not just hidden in UI state. Validation - make proto - go test ./services/control-plane/internal/session ./services/control-plane/internal/api ./clients/cli/... - go test ./services/control-plane/... - make run-macos (build succeeded) - Manual smoke check via CLI: - port unexpose returns success - events stream includes port_unexposed entry --- clients/cli/cmd/events.go | 5 + clients/cli/cmd/port.go | 76 ++ .../ios/Netclode/Features/Chat/ChatView.swift | 6 + .../Features/Chat/ToolEventCard.swift | 27 +- .../Features/Workspace/PreviewsView.swift | 32 +- .../Generated/netclode/v1/client.pb.swift | 194 ++++- .../Generated/netclode/v1/events.pb.swift | 80 ++- clients/ios/Netclode/Models/AgentEvent.swift | 14 + .../ios/Netclode/Models/ClientMessage.swift | 6 + .../Netclode/Models/PersistedMessage.swift | 7 + .../ios/Netclode/Models/ServerMessage.swift | 13 + .../Netclode/Services/ConnectService.swift | 24 +- .../ios/Netclode/Services/MessageRouter.swift | 3 + proto/netclode/v1/client.proto | 14 + proto/netclode/v1/events.proto | 7 + services/agent/gen/netclode/v1/client_pb.ts | 140 +++- services/agent/gen/netclode/v1/events_pb.ts | 38 +- .../gen/netclode/v1/client.pb.go | 664 +++++++++++------- .../gen/netclode/v1/events.pb.go | 135 +++- .../internal/api/connect_client.go | 24 + services/control-plane/internal/k8s/client.go | 1 + .../control-plane/internal/k8s/sandbox.go | 98 +++ .../control-plane/internal/session/manager.go | 41 +- .../internal/session/manager_test.go | 177 ++++- 24 files changed, 1486 insertions(+), 340 deletions(-) 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/ios/Netclode/Features/Chat/ChatView.swift b/clients/ios/Netclode/Features/Chat/ChatView.swift index e660fc06..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: { 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/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/client.pb.swift b/clients/ios/Netclode/Generated/netclode/v1/client.pb.swift index 3f4c9f4f..cb49b40f 100644 --- a/clients/ios/Netclode/Generated/netclode/v1/client.pb.swift +++ b/clients/ios/Netclode/Generated/netclode/v1/client.pb.swift @@ -278,6 +278,14 @@ public struct Netclode_V1_ClientMessage: Sendable { 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 { @@ -310,6 +318,7 @@ public struct Netclode_V1_ClientMessage: Sendable { case codexAuthStart(Netclode_V1_CodexAuthStartRequest) case codexAuthStatus(Netclode_V1_CodexAuthStatusRequest) case codexAuthLogout(Netclode_V1_CodexAuthLogoutRequest) + case unexposePort(Netclode_V1_UnexposePortRequest) } @@ -514,6 +523,14 @@ public struct Netclode_V1_ServerMessage: Sendable { 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 { @@ -546,6 +563,7 @@ public struct Netclode_V1_ServerMessage: Sendable { case codexAuthStarted(Netclode_V1_CodexAuthStartedResponse) case codexAuthStatus(Netclode_V1_CodexAuthStatusResponse) case codexAuthLoggedOut(Netclode_V1_CodexAuthLoggedOutResponse) + case portUnexposed(Netclode_V1_PortUnexposedResponse) } @@ -1001,6 +1019,31 @@ public struct Netclode_V1_ExposePortRequest: Sendable { 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 {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_SyncRequest: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for @@ -1597,6 +1640,31 @@ public struct Netclode_V1_PortExposedResponse: Sendable { 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 {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_GitHubReposResponse: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for @@ -2055,7 +2123,7 @@ extension Netclode_V1_CodexAuthState: SwiftProtobuf._ProtoNameProviding { 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\u{3}codex_auth_start\0\u{3}codex_auth_status\0\u{3}codex_auth_logout\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() { @@ -2388,6 +2456,19 @@ extension Netclode_V1_ClientMessage: SwiftProtobuf.Message, SwiftProtobuf._Messa 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 } } @@ -2499,6 +2580,10 @@ extension Netclode_V1_ClientMessage: SwiftProtobuf.Message, SwiftProtobuf._Messa 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) @@ -2513,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\u{3}codex_auth_started\0\u{3}codex_auth_status\0\u{3}codex_auth_logged_out\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() { @@ -2820,6 +2905,19 @@ extension Netclode_V1_ServerMessage: SwiftProtobuf.Message, SwiftProtobuf._Messa 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 } } @@ -2923,6 +3021,10 @@ extension Netclode_V1_ServerMessage: SwiftProtobuf.Message, SwiftProtobuf._Messa 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) @@ -3552,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") @@ -4514,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") diff --git a/clients/ios/Netclode/Generated/netclode/v1/events.pb.swift b/clients/ios/Netclode/Generated/netclode/v1/events.pb.swift index 7f3dbd63..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) } @@ -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 cfca95e9..bbd24d1f 100644 --- a/clients/ios/Netclode/Services/ConnectService.swift +++ b/clients/ios/Netclode/Services/ConnectService.swift @@ -464,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) }) @@ -754,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 @@ -910,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) @@ -1020,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" @@ -1369,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 7b4cdaea..8687324d 100644 --- a/clients/ios/Netclode/Services/MessageRouter.swift +++ b/clients/ios/Netclode/Services/MessageRouter.swift @@ -268,6 +268,9 @@ 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)") diff --git a/proto/netclode/v1/client.proto b/proto/netclode/v1/client.proto index 955d67a8..b6672c5d 100644 --- a/proto/netclode/v1/client.proto +++ b/proto/netclode/v1/client.proto @@ -45,6 +45,7 @@ message ClientMessage { CodexAuthStartRequest codex_auth_start = 23; CodexAuthStatusRequest codex_auth_status = 24; CodexAuthLogoutRequest codex_auth_logout = 25; + UnexposePortRequest unexpose_port = 26; } } @@ -80,6 +81,7 @@ message ServerMessage { CodexAuthStartedResponse codex_auth_started = 25; CodexAuthStatusResponse codex_auth_status = 26; CodexAuthLoggedOutResponse codex_auth_logged_out = 27; + PortUnexposedResponse port_unexposed = 28; } } @@ -177,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; } @@ -297,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; 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/services/agent/gen/netclode/v1/client_pb.ts b/services/agent/gen/netclode/v1/client_pb.ts index 2f487a8c..14ad07f3 100644 --- a/services/agent/gen/netclode/v1/client_pb.ts +++ b/services/agent/gen/netclode/v1/client_pb.ts @@ -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("ChhuZXRjbG9kZS92MS9jbGllbnQucHJvdG8SC25ldGNsb2RlLnYxIooMCg1DbGllbnRNZXNzYWdlEjsKDmNyZWF0ZV9zZXNzaW9uGAEgASgLMiEubmV0Y2xvZGUudjEuQ3JlYXRlU2Vzc2lvblJlcXVlc3RIABI5Cg1saXN0X3Nlc3Npb25zGAIgASgLMiAubmV0Y2xvZGUudjEuTGlzdFNlc3Npb25zUmVxdWVzdEgAEjcKDG9wZW5fc2Vzc2lvbhgDIAEoCzIfLm5ldGNsb2RlLnYxLk9wZW5TZXNzaW9uUmVxdWVzdEgAEjsKDnJlc3VtZV9zZXNzaW9uGAQgASgLMiEubmV0Y2xvZGUudjEuUmVzdW1lU2Vzc2lvblJlcXVlc3RIABI5Cg1wYXVzZV9zZXNzaW9uGAUgASgLMiAubmV0Y2xvZGUudjEuUGF1c2VTZXNzaW9uUmVxdWVzdEgAEjsKDmRlbGV0ZV9zZXNzaW9uGAYgASgLMiEubmV0Y2xvZGUudjEuRGVsZXRlU2Vzc2lvblJlcXVlc3RIABJEChNkZWxldGVfYWxsX3Nlc3Npb25zGAcgASgLMiUubmV0Y2xvZGUudjEuRGVsZXRlQWxsU2Vzc2lvbnNSZXF1ZXN0SAASNQoLc2VuZF9wcm9tcHQYCCABKAsyHi5uZXRjbG9kZS52MS5TZW5kUHJvbXB0UmVxdWVzdEgAEj8KEGludGVycnVwdF9wcm9tcHQYCSABKAsyIy5uZXRjbG9kZS52MS5JbnRlcnJ1cHRQcm9tcHRSZXF1ZXN0SAASOwoOdGVybWluYWxfaW5wdXQYCiABKAsyIS5uZXRjbG9kZS52MS5UZXJtaW5hbElucHV0UmVxdWVzdEgAEj0KD3Rlcm1pbmFsX3Jlc2l6ZRgLIAEoCzIiLm5ldGNsb2RlLnYxLlRlcm1pbmFsUmVzaXplUmVxdWVzdEgAEjUKC2V4cG9zZV9wb3J0GAwgASgLMh4ubmV0Y2xvZGUudjEuRXhwb3NlUG9ydFJlcXVlc3RIABIoCgRzeW5jGA0gASgLMhgubmV0Y2xvZGUudjEuU3luY1JlcXVlc3RIABJAChFsaXN0X2dpdGh1Yl9yZXBvcxgOIAEoCzIjLm5ldGNsb2RlLnYxLkxpc3RHaXRIdWJSZXBvc1JlcXVlc3RIABIzCgpnaXRfc3RhdHVzGA8gASgLMh0ubmV0Y2xvZGUudjEuR2l0U3RhdHVzUmVxdWVzdEgAEi8KCGdpdF9kaWZmGBAgASgLMhsubmV0Y2xvZGUudjEuR2l0RGlmZlJlcXVlc3RIABI1CgtsaXN0X21vZGVscxgRIAEoCzIeLm5ldGNsb2RlLnYxLkxpc3RNb2RlbHNSZXF1ZXN0SAASQgoSZ2V0X2NvcGlsb3Rfc3RhdHVzGBIgASgLMiQubmV0Y2xvZGUudjEuR2V0Q29waWxvdFN0YXR1c1JlcXVlc3RIABI7Cg5saXN0X3NuYXBzaG90cxgTIAEoCzIhLm5ldGNsb2RlLnYxLkxpc3RTbmFwc2hvdHNSZXF1ZXN0SAASPwoQcmVzdG9yZV9zbmFwc2hvdBgUIAEoCzIjLm5ldGNsb2RlLnYxLlJlc3RvcmVTbmFwc2hvdFJlcXVlc3RIABJCChJ1cGRhdGVfcmVwb19hY2Nlc3MYFSABKAsyJC5uZXRjbG9kZS52MS5VcGRhdGVSZXBvQWNjZXNzUmVxdWVzdEgAEkQKE2dldF9yZXNvdXJjZV9saW1pdHMYFiABKAsyJS5uZXRjbG9kZS52MS5HZXRSZXNvdXJjZUxpbWl0c1JlcXVlc3RIABI+ChBjb2RleF9hdXRoX3N0YXJ0GBcgASgLMiIubmV0Y2xvZGUudjEuQ29kZXhBdXRoU3RhcnRSZXF1ZXN0SAASQAoRY29kZXhfYXV0aF9zdGF0dXMYGCABKAsyIy5uZXRjbG9kZS52MS5Db2RleEF1dGhTdGF0dXNSZXF1ZXN0SAASQAoRY29kZXhfYXV0aF9sb2dvdXQYGSABKAsyIy5uZXRjbG9kZS52MS5Db2RleEF1dGhMb2dvdXRSZXF1ZXN0SABCCQoHbWVzc2FnZSKYCwoNU2VydmVyTWVzc2FnZRI+Cg9zZXNzaW9uX2NyZWF0ZWQYASABKAsyIy5uZXRjbG9kZS52MS5TZXNzaW9uQ3JlYXRlZFJlc3BvbnNlSAASPgoPc2Vzc2lvbl91cGRhdGVkGAIgASgLMiMubmV0Y2xvZGUudjEuU2Vzc2lvblVwZGF0ZWRSZXNwb25zZUgAEj4KD3Nlc3Npb25fZGVsZXRlZBgDIAEoCzIjLm5ldGNsb2RlLnYxLlNlc3Npb25EZWxldGVkUmVzcG9uc2VIABJHChRzZXNzaW9uc19kZWxldGVkX2FsbBgEIAEoCzInLm5ldGNsb2RlLnYxLlNlc3Npb25zRGVsZXRlZEFsbFJlc3BvbnNlSAASOAoMc2Vzc2lvbl9saXN0GAUgASgLMiAubmV0Y2xvZGUudjEuU2Vzc2lvbkxpc3RSZXNwb25zZUgAEjoKDXNlc3Npb25fc3RhdGUYBiABKAsyIS5uZXRjbG9kZS52MS5TZXNzaW9uU3RhdGVSZXNwb25zZUgAEjIKDXN5bmNfcmVzcG9uc2UYByABKAsyGS5uZXRjbG9kZS52MS5TeW5jUmVzcG9uc2VIABI4CgxzdHJlYW1fZW50cnkYCCABKAsyIC5uZXRjbG9kZS52MS5TdHJlYW1FbnRyeVJlc3BvbnNlSAASOAoMcG9ydF9leHBvc2VkGA0gASgLMiAubmV0Y2xvZGUudjEuUG9ydEV4cG9zZWRSZXNwb25zZUgAEjgKDGdpdGh1Yl9yZXBvcxgOIAEoCzIgLm5ldGNsb2RlLnYxLkdpdEh1YlJlcG9zUmVzcG9uc2VIABI0CgpnaXRfc3RhdHVzGA8gASgLMh4ubmV0Y2xvZGUudjEuR2l0U3RhdHVzUmVzcG9uc2VIABIwCghnaXRfZGlmZhgQIAEoCzIcLm5ldGNsb2RlLnYxLkdpdERpZmZSZXNwb25zZUgAEisKBWVycm9yGBEgASgLMhoubmV0Y2xvZGUudjEuRXJyb3JSZXNwb25zZUgAEi0KBm1vZGVscxgSIAEoCzIbLm5ldGNsb2RlLnYxLk1vZGVsc1Jlc3BvbnNlSAASPAoOY29waWxvdF9zdGF0dXMYEyABKAsyIi5uZXRjbG9kZS52MS5Db3BpbG90U3RhdHVzUmVzcG9uc2VIABJAChBzbmFwc2hvdF9jcmVhdGVkGBQgASgLMiQubmV0Y2xvZGUudjEuU25hcHNob3RDcmVhdGVkUmVzcG9uc2VIABI6Cg1zbmFwc2hvdF9saXN0GBUgASgLMiEubmV0Y2xvZGUudjEuU25hcHNob3RMaXN0UmVzcG9uc2VIABJCChFzbmFwc2hvdF9yZXN0b3JlZBgWIAEoCzIlLm5ldGNsb2RlLnYxLlNuYXBzaG90UmVzdG9yZWRSZXNwb25zZUgAEkUKE3JlcG9fYWNjZXNzX3VwZGF0ZWQYFyABKAsyJi5uZXRjbG9kZS52MS5SZXBvQWNjZXNzVXBkYXRlZFJlc3BvbnNlSAASPgoPcmVzb3VyY2VfbGltaXRzGBggASgLMiMubmV0Y2xvZGUudjEuUmVzb3VyY2VMaW1pdHNSZXNwb25zZUgAEkMKEmNvZGV4X2F1dGhfc3RhcnRlZBgZIAEoCzIlLm5ldGNsb2RlLnYxLkNvZGV4QXV0aFN0YXJ0ZWRSZXNwb25zZUgAEkEKEWNvZGV4X2F1dGhfc3RhdHVzGBogASgLMiQubmV0Y2xvZGUudjEuQ29kZXhBdXRoU3RhdHVzUmVzcG9uc2VIABJIChVjb2RleF9hdXRoX2xvZ2dlZF9vdXQYGyABKAsyJy5uZXRjbG9kZS52MS5Db2RleEF1dGhMb2dnZWRPdXRSZXNwb25zZUgAQgkKB21lc3NhZ2UiJwoNTmV0d29ya0NvbmZpZxIWCg50YWlsbmV0X2FjY2VzcxgBIAEoCCKVAQoQQ29kZXhPQXV0aFRva2VucxIUCgxhY2Nlc3NfdG9rZW4YASABKAkSEAoIaWRfdG9rZW4YAiABKAkSFQoNcmVmcmVzaF90b2tlbhgDIAEoCRIzCgpleHBpcmVzX2F0GAQgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcEgAiAEBQg0KC19leHBpcmVzX2F0IusEChRDcmVhdGVTZXNzaW9uUmVxdWVzdBIXCgpyZXF1ZXN0X2lkGAEgASgJSACIAQESEQoEbmFtZRgCIAEoCUgBiAEBEg0KBXJlcG9zGAMgAygJEjEKC3JlcG9fYWNjZXNzGAQgASgOMhcubmV0Y2xvZGUudjEuUmVwb0FjY2Vzc0gCiAEBEhsKDmluaXRpYWxfcHJvbXB0GAUgASgJSAOIAQESKwoIc2RrX3R5cGUYBiABKA4yFC5uZXRjbG9kZS52MS5TZGtUeXBlSASIAQESEgoFbW9kZWwYByABKAlIBYgBARI5Cg9jb3BpbG90X2JhY2tlbmQYCCABKA4yGy5uZXRjbG9kZS52MS5Db3BpbG90QmFja2VuZEgGiAEBEjcKDm5ldHdvcmtfY29uZmlnGAkgASgLMhoubmV0Y2xvZGUudjEuTmV0d29ya0NvbmZpZ0gHiAEBEjUKCXJlc291cmNlcxgKIAEoCzIdLm5ldGNsb2RlLnYxLlNhbmRib3hSZXNvdXJjZXNICIgBARI+ChJjb2RleF9vYXV0aF90b2tlbnMYCyABKAsyHS5uZXRjbG9kZS52MS5Db2RleE9BdXRoVG9rZW5zSAmIAQFCDQoLX3JlcXVlc3RfaWRCBwoFX25hbWVCDgoMX3JlcG9fYWNjZXNzQhEKD19pbml0aWFsX3Byb21wdEILCglfc2RrX3R5cGVCCAoGX21vZGVsQhIKEF9jb3BpbG90X2JhY2tlbmRCEQoPX25ldHdvcmtfY29uZmlnQgwKCl9yZXNvdXJjZXNCFQoTX2NvZGV4X29hdXRoX3Rva2VucyI9ChNMaXN0U2Vzc2lvbnNSZXF1ZXN0EhcKCnJlcXVlc3RfaWQYASABKAlIAIgBAUINCgtfcmVxdWVzdF9pZCKgAQoST3BlblNlc3Npb25SZXF1ZXN0EhcKCnJlcXVlc3RfaWQYASABKAlIAIgBARISCgpzZXNzaW9uX2lkGAIgASgJEhwKD2FmdGVyX3N0cmVhbV9pZBgDIAEoCUgBiAEBEhIKBWxpbWl0GAQgASgFSAKIAQFCDQoLX3JlcXVlc3RfaWRCEgoQX2FmdGVyX3N0cmVhbV9pZEIICgZfbGltaXQiUgoUUmVzdW1lU2Vzc2lvblJlcXVlc3QSFwoKcmVxdWVzdF9pZBgBIAEoCUgAiAEBEhIKCnNlc3Npb25faWQYAiABKAlCDQoLX3JlcXVlc3RfaWQiUQoTUGF1c2VTZXNzaW9uUmVxdWVzdBIXCgpyZXF1ZXN0X2lkGAEgASgJSACIAQESEgoKc2Vzc2lvbl9pZBgCIAEoCUINCgtfcmVxdWVzdF9pZCJSChREZWxldGVTZXNzaW9uUmVxdWVzdBIXCgpyZXF1ZXN0X2lkGAEgASgJSACIAQESEgoKc2Vzc2lvbl9pZBgCIAEoCUINCgtfcmVxdWVzdF9pZCJCChhEZWxldGVBbGxTZXNzaW9uc1JlcXVlc3QSFwoKcmVxdWVzdF9pZBgBIAEoCUgAiAEBQg0KC19yZXF1ZXN0X2lkIl0KEVNlbmRQcm9tcHRSZXF1ZXN0EhcKCnJlcXVlc3RfaWQYASABKAlIAIgBARISCgpzZXNzaW9uX2lkGAIgASgJEgwKBHRleHQYAyABKAlCDQoLX3JlcXVlc3RfaWQiVAoWSW50ZXJydXB0UHJvbXB0UmVxdWVzdBIXCgpyZXF1ZXN0X2lkGAEgASgJSACIAQESEgoKc2Vzc2lvbl9pZBgCIAEoCUINCgtfcmVxdWVzdF9pZCJgChRUZXJtaW5hbElucHV0UmVxdWVzdBIXCgpyZXF1ZXN0X2lkGAEgASgJSACIAQESEgoKc2Vzc2lvbl9pZBgCIAEoCRIMCgRkYXRhGAMgASgJQg0KC19yZXF1ZXN0X2lkIm8KFVRlcm1pbmFsUmVzaXplUmVxdWVzdBIXCgpyZXF1ZXN0X2lkGAEgASgJSACIAQESEgoKc2Vzc2lvbl9pZBgCIAEoCRIMCgRjb2xzGAMgASgFEgwKBHJvd3MYBCABKAVCDQoLX3JlcXVlc3RfaWQiXQoRRXhwb3NlUG9ydFJlcXVlc3QSFwoKcmVxdWVzdF9pZBgBIAEoCUgAiAEBEhIKCnNlc3Npb25faWQYAiABKAkSDAoEcG9ydBgDIAEoBUINCgtfcmVxdWVzdF9pZCI1CgtTeW5jUmVxdWVzdBIXCgpyZXF1ZXN0X2lkGAEgASgJSACIAQFCDQoLX3JlcXVlc3RfaWQiQAoWTGlzdEdpdEh1YlJlcG9zUmVxdWVzdBIXCgpyZXF1ZXN0X2lkGAEgASgJSACIAQFCDQoLX3JlcXVlc3RfaWQiTgoQR2l0U3RhdHVzUmVxdWVzdBIXCgpyZXF1ZXN0X2lkGAEgASgJSACIAQESEgoKc2Vzc2lvbl9pZBgCIAEoCUINCgtfcmVxdWVzdF9pZCJoCg5HaXREaWZmUmVxdWVzdBIXCgpyZXF1ZXN0X2lkGAEgASgJSACIAQESEgoKc2Vzc2lvbl9pZBgCIAEoCRIRCgRmaWxlGAMgASgJSAGIAQFCDQoLX3JlcXVlc3RfaWRCBwoFX2ZpbGUi8AEKEUxpc3RNb2RlbHNSZXF1ZXN0EhcKCnJlcXVlc3RfaWQYASABKAlIAIgBARImCghzZGtfdHlwZRgCIAEoDjIULm5ldGNsb2RlLnYxLlNka1R5cGUSOQoPY29waWxvdF9iYWNrZW5kGAMgASgOMhsubmV0Y2xvZGUudjEuQ29waWxvdEJhY2tlbmRIAYgBARIiChVjb2RleF9vYXV0aF9hdmFpbGFibGUYBCABKAhIAogBAUINCgtfcmVxdWVzdF9pZEISChBfY29waWxvdF9iYWNrZW5kQhgKFl9jb2RleF9vYXV0aF9hdmFpbGFibGUiQQoXR2V0Q29waWxvdFN0YXR1c1JlcXVlc3QSFwoKcmVxdWVzdF9pZBgBIAEoCUgAiAEBQg0KC19yZXF1ZXN0X2lkIlIKFExpc3RTbmFwc2hvdHNSZXF1ZXN0EhcKCnJlcXVlc3RfaWQYASABKAlIAIgBARISCgpzZXNzaW9uX2lkGAIgASgJQg0KC19yZXF1ZXN0X2lkImkKFlJlc3RvcmVTbmFwc2hvdFJlcXVlc3QSFwoKcmVxdWVzdF9pZBgBIAEoCUgAiAEBEhIKCnNlc3Npb25faWQYAiABKAkSEwoLc25hcHNob3RfaWQYAyABKAlCDQoLX3JlcXVlc3RfaWQigwEKF1VwZGF0ZVJlcG9BY2Nlc3NSZXF1ZXN0EhcKCnJlcXVlc3RfaWQYASABKAlIAIgBARISCgpzZXNzaW9uX2lkGAIgASgJEiwKC3JlcG9fYWNjZXNzGAMgASgOMhcubmV0Y2xvZGUudjEuUmVwb0FjY2Vzc0INCgtfcmVxdWVzdF9pZCJCChhHZXRSZXNvdXJjZUxpbWl0c1JlcXVlc3QSFwoKcmVxdWVzdF9pZBgBIAEoCUgAiAEBQg0KC19yZXF1ZXN0X2lkIj8KFUNvZGV4QXV0aFN0YXJ0UmVxdWVzdBIXCgpyZXF1ZXN0X2lkGAEgASgJSACIAQFCDQoLX3JlcXVlc3RfaWQiQAoWQ29kZXhBdXRoU3RhdHVzUmVxdWVzdBIXCgpyZXF1ZXN0X2lkGAEgASgJSACIAQFCDQoLX3JlcXVlc3RfaWQiQAoWQ29kZXhBdXRoTG9nb3V0UmVxdWVzdBIXCgpyZXF1ZXN0X2lkGAEgASgJSACIAQFCDQoLX3JlcXVlc3RfaWQiZwoWU2Vzc2lvbkNyZWF0ZWRSZXNwb25zZRIlCgdzZXNzaW9uGAEgASgLMhQubmV0Y2xvZGUudjEuU2Vzc2lvbhIXCgpyZXF1ZXN0X2lkGAIgASgJSACIAQFCDQoLX3JlcXVlc3RfaWQiPwoWU2Vzc2lvblVwZGF0ZWRSZXNwb25zZRIlCgdzZXNzaW9uGAEgASgLMhQubmV0Y2xvZGUudjEuU2Vzc2lvbiJUChZTZXNzaW9uRGVsZXRlZFJlc3BvbnNlEhIKCnNlc3Npb25faWQYASABKAkSFwoKcmVxdWVzdF9pZBgCIAEoCUgAiAEBQg0KC19yZXF1ZXN0X2lkIlkKGlNlc3Npb25zRGVsZXRlZEFsbFJlc3BvbnNlEhMKC2RlbGV0ZWRfaWRzGAEgAygJEhcKCnJlcXVlc3RfaWQYAiABKAlIAIgBAUINCgtfcmVxdWVzdF9pZCJlChNTZXNzaW9uTGlzdFJlc3BvbnNlEiYKCHNlc3Npb25zGAEgAygLMhQubmV0Y2xvZGUudjEuU2Vzc2lvbhIXCgpyZXF1ZXN0X2lkGAIgASgJSACIAQFCDQoLX3JlcXVlc3RfaWQimgIKFFNlc3Npb25TdGF0ZVJlc3BvbnNlEiUKB3Nlc3Npb24YASABKAsyFC5uZXRjbG9kZS52MS5TZXNzaW9uEikKB2VudHJpZXMYAiADKAsyGC5uZXRjbG9kZS52MS5TdHJlYW1FbnRyeRIQCghoYXNfbW9yZRgDIAEoCBIbCg5sYXN0X3N0cmVhbV9pZBgEIAEoCUgAiAEBEjYKC2luX3Byb2dyZXNzGAUgASgLMhwubmV0Y2xvZGUudjEuSW5Qcm9ncmVzc1N0YXRlSAGIAQESFwoKcmVxdWVzdF9pZBgGIAEoCUgCiAEBQhEKD19sYXN0X3N0cmVhbV9pZEIOCgxfaW5fcHJvZ3Jlc3NCDQoLX3JlcXVlc3RfaWQilgEKDFN5bmNSZXNwb25zZRItCghzZXNzaW9ucxgBIAMoCzIbLm5ldGNsb2RlLnYxLlNlc3Npb25TdW1tYXJ5Ei8KC3NlcnZlcl90aW1lGAIgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcBIXCgpyZXF1ZXN0X2lkGAMgASgJSACIAQFCDQoLX3JlcXVlc3RfaWQiUgoTU3RyZWFtRW50cnlSZXNwb25zZRISCgpzZXNzaW9uX2lkGAEgASgJEicKBWVudHJ5GAIgASgLMhgubmV0Y2xvZGUudjEuU3RyZWFtRW50cnkidAoTUG9ydEV4cG9zZWRSZXNwb25zZRISCgpzZXNzaW9uX2lkGAEgASgJEgwKBHBvcnQYAiABKAUSEwoLcHJldmlld191cmwYAyABKAkSFwoKcmVxdWVzdF9pZBgEIAEoCUgAiAEBQg0KC19yZXF1ZXN0X2lkImUKE0dpdEh1YlJlcG9zUmVzcG9uc2USJgoFcmVwb3MYASADKAsyFy5uZXRjbG9kZS52MS5HaXRIdWJSZXBvEhcKCnJlcXVlc3RfaWQYAiABKAlIAIgBAUINCgtfcmVxdWVzdF9pZCJ6ChFHaXRTdGF0dXNSZXNwb25zZRISCgpzZXNzaW9uX2lkGAEgASgJEikKBWZpbGVzGAIgAygLMhoubmV0Y2xvZGUudjEuR2l0RmlsZUNoYW5nZRIXCgpyZXF1ZXN0X2lkGAMgASgJSACIAQFCDQoLX3JlcXVlc3RfaWQiWwoPR2l0RGlmZlJlc3BvbnNlEhIKCnNlc3Npb25faWQYASABKAkSDAoEZGlmZhgCIAEoCRIXCgpyZXF1ZXN0X2lkGAMgASgJSACIAQFCDQoLX3JlcXVlc3RfaWQiWgoNRXJyb3JSZXNwb25zZRIhCgVlcnJvchgBIAEoCzISLm5ldGNsb2RlLnYxLkVycm9yEhcKCnJlcXVlc3RfaWQYAiABKAlIAIgBAUINCgtfcmVxdWVzdF9pZCKaAQoOTW9kZWxzUmVzcG9uc2USJgoGbW9kZWxzGAEgAygLMhYubmV0Y2xvZGUudjEuTW9kZWxJbmZvEhcKCnJlcXVlc3RfaWQYAiABKAlIAIgBARIrCghzZGtfdHlwZRgDIAEoDjIULm5ldGNsb2RlLnYxLlNka1R5cGVIAYgBAUINCgtfcmVxdWVzdF9pZEILCglfc2RrX3R5cGUirQEKFUNvcGlsb3RTdGF0dXNSZXNwb25zZRIsCgRhdXRoGAEgASgLMh4ubmV0Y2xvZGUudjEuQ29waWxvdEF1dGhTdGF0dXMSNAoFcXVvdGEYAiABKAsyIC5uZXRjbG9kZS52MS5Db3BpbG90UHJlbWl1bVF1b3RhSACIAQESFwoKcmVxdWVzdF9pZBgDIAEoCUgBiAEBQggKBl9xdW90YUINCgtfcmVxdWVzdF9pZCL/AQoYQ29kZXhBdXRoU3RhcnRlZFJlc3BvbnNlEhgKEHZlcmlmaWNhdGlvbl91cmkYASABKAkSJgoZdmVyaWZpY2F0aW9uX3VyaV9jb21wbGV0ZRgCIAEoCUgAiAEBEhEKCXVzZXJfY29kZRgDIAEoCRIYChBpbnRlcnZhbF9zZWNvbmRzGAQgASgFEi4KCmV4cGlyZXNfYXQYBSABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEhcKCnJlcXVlc3RfaWQYBiABKAlIAYgBAUIcChpfdmVyaWZpY2F0aW9uX3VyaV9jb21wbGV0ZUINCgtfcmVxdWVzdF9pZCL3AQoXQ29kZXhBdXRoU3RhdHVzUmVzcG9uc2USKgoFc3RhdGUYASABKA4yGy5uZXRjbG9kZS52MS5Db2RleEF1dGhTdGF0ZRIXCgphY2NvdW50X2lkGAIgASgJSACIAQESMwoKZXhwaXJlc19hdBgDIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXBIAYgBARISCgVlcnJvchgEIAEoCUgCiAEBEhcKCnJlcXVlc3RfaWQYBSABKAlIA4gBAUINCgtfYWNjb3VudF9pZEINCgtfZXhwaXJlc19hdEIICgZfZXJyb3JCDQoLX3JlcXVlc3RfaWQiRAoaQ29kZXhBdXRoTG9nZ2VkT3V0UmVzcG9uc2USFwoKcmVxdWVzdF9pZBgBIAEoCUgAiAEBQg0KC19yZXF1ZXN0X2lkIlYKF1NuYXBzaG90Q3JlYXRlZFJlc3BvbnNlEhIKCnNlc3Npb25faWQYASABKAkSJwoIc25hcHNob3QYAiABKAsyFS5uZXRjbG9kZS52MS5TbmFwc2hvdCJ8ChRTbmFwc2hvdExpc3RSZXNwb25zZRISCgpzZXNzaW9uX2lkGAEgASgJEigKCXNuYXBzaG90cxgCIAMoCzIVLm5ldGNsb2RlLnYxLlNuYXBzaG90EhcKCnJlcXVlc3RfaWQYAyABKAlIAIgBAUINCgtfcmVxdWVzdF9pZCKGAQoYU25hcHNob3RSZXN0b3JlZFJlc3BvbnNlEhIKCnNlc3Npb25faWQYASABKAkSEwoLc25hcHNob3RfaWQYAiABKAkSGQoRbWVzc2FnZXNfcmVzdG9yZWQYAyABKAUSFwoKcmVxdWVzdF9pZBgEIAEoCUgAiAEBQg0KC19yZXF1ZXN0X2lkIoUBChlSZXBvQWNjZXNzVXBkYXRlZFJlc3BvbnNlEhIKCnNlc3Npb25faWQYASABKAkSLAoLcmVwb19hY2Nlc3MYAiABKA4yFy5uZXRjbG9kZS52MS5SZXBvQWNjZXNzEhcKCnJlcXVlc3RfaWQYAyABKAlIAIgBAUINCgtfcmVxdWVzdF9pZCKcAQoWUmVzb3VyY2VMaW1pdHNSZXNwb25zZRIRCgltYXhfdmNwdXMYASABKAUSFQoNbWF4X21lbW9yeV9tYhgCIAEoBRIVCg1kZWZhdWx0X3ZjcHVzGAMgASgFEhkKEWRlZmF1bHRfbWVtb3J5X21iGAQgASgFEhcKCnJlcXVlc3RfaWQYBSABKAlIAIgBAUINCgtfcmVxdWVzdF9pZCquAQoOQ29kZXhBdXRoU3RhdGUSIAocQ09ERVhfQVVUSF9TVEFURV9VTlNQRUNJRklFRBAAEiQKIENPREVYX0FVVEhfU1RBVEVfVU5BVVRIRU5USUNBVEVEEAESHAoYQ09ERVhfQVVUSF9TVEFURV9QRU5ESU5HEAISGgoWQ09ERVhfQVVUSF9TVEFURV9SRUFEWRADEhoKFkNPREVYX0FVVEhfU1RBVEVfRVJST1IQBDJWCg1DbGllbnRTZXJ2aWNlEkUKB0Nvbm5lY3QSGi5uZXRjbG9kZS52MS5DbGllbnRNZXNzYWdlGhoubmV0Y2xvZGUudjEuU2VydmVyTWVzc2FnZSgBMAFCvAEKD2NvbS5uZXRjbG9kZS52MUILQ2xpZW50UHJvdG9QAVpPZ2l0aHViLmNvbS9hbmdyaXN0YW4vbmV0Y2xvZGUvc2VydmljZXMvY29udHJvbC1wbGFuZS9nZW4vbmV0Y2xvZGUvdjE7bmV0Y2xvZGV2MaICA05YWKoCC05ldGNsb2RlLlYxygILTmV0Y2xvZGVcVjHiAhdOZXRjbG9kZVxWMVxHUEJNZXRhZGF0YeoCDE5ldGNsb2RlOjpWMWIGcHJvdG8z", [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. @@ -184,6 +184,12 @@ export type ClientMessage = Message<"netclode.v1.ClientMessage"> & { */ value: CodexAuthLogoutRequest; case: "codexAuthLogout"; + } | { + /** + * @generated from field: netclode.v1.UnexposePortRequest unexpose_port = 26; + */ + value: UnexposePortRequest; + case: "unexposePort"; } | { case: undefined; value?: undefined }; }; @@ -355,6 +361,12 @@ export type ServerMessage = Message<"netclode.v1.ServerMessage"> & { */ value: CodexAuthLoggedOutResponse; case: "codexAuthLoggedOut"; + } | { + /** + * @generated from field: netclode.v1.PortUnexposedResponse port_unexposed = 28; + */ + value: PortUnexposedResponse; + case: "portUnexposed"; } | { case: undefined; value?: undefined }; }; @@ -782,6 +794,33 @@ export type ExposePortRequest = Message<"netclode.v1.ExposePortRequest"> & { export const ExposePortRequestSchema: GenMessage = /*@__PURE__*/ 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 */ @@ -797,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, 16); + messageDesc(file_netclode_v1_client, 17); /** * @generated from message netclode.v1.ListGitHubReposRequest @@ -814,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, 17); + messageDesc(file_netclode_v1_client, 18); /** * @generated from message netclode.v1.GitStatusRequest @@ -836,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, 18); + messageDesc(file_netclode_v1_client, 19); /** * @generated from message netclode.v1.GitDiffRequest @@ -865,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, 19); + messageDesc(file_netclode_v1_client, 20); /** * @generated from message netclode.v1.ListModelsRequest @@ -903,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, 20); + messageDesc(file_netclode_v1_client, 21); /** * @generated from message netclode.v1.GetCopilotStatusRequest @@ -920,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, 21); + messageDesc(file_netclode_v1_client, 22); /** * @generated from message netclode.v1.ListSnapshotsRequest @@ -942,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, 22); + messageDesc(file_netclode_v1_client, 23); /** * @generated from message netclode.v1.RestoreSnapshotRequest @@ -969,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, 23); + messageDesc(file_netclode_v1_client, 24); /** * @generated from message netclode.v1.UpdateRepoAccessRequest @@ -998,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, 24); + messageDesc(file_netclode_v1_client, 25); /** * @generated from message netclode.v1.GetResourceLimitsRequest @@ -1015,7 +1054,7 @@ 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, 25); + messageDesc(file_netclode_v1_client, 26); /** * @generated from message netclode.v1.CodexAuthStartRequest @@ -1032,7 +1071,7 @@ export type CodexAuthStartRequest = Message<"netclode.v1.CodexAuthStartRequest"> * Use `create(CodexAuthStartRequestSchema)` to create a new message. */ export const CodexAuthStartRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_netclode_v1_client, 26); + messageDesc(file_netclode_v1_client, 27); /** * @generated from message netclode.v1.CodexAuthStatusRequest @@ -1049,7 +1088,7 @@ export type CodexAuthStatusRequest = Message<"netclode.v1.CodexAuthStatusRequest * Use `create(CodexAuthStatusRequestSchema)` to create a new message. */ export const CodexAuthStatusRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_netclode_v1_client, 27); + messageDesc(file_netclode_v1_client, 28); /** * @generated from message netclode.v1.CodexAuthLogoutRequest @@ -1066,7 +1105,7 @@ export type CodexAuthLogoutRequest = Message<"netclode.v1.CodexAuthLogoutRequest * Use `create(CodexAuthLogoutRequestSchema)` to create a new message. */ export const CodexAuthLogoutRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_netclode_v1_client, 28); + messageDesc(file_netclode_v1_client, 29); /** * @generated from message netclode.v1.SessionCreatedResponse @@ -1090,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, 29); + messageDesc(file_netclode_v1_client, 30); /** * @generated from message netclode.v1.SessionUpdatedResponse @@ -1107,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, 30); + messageDesc(file_netclode_v1_client, 31); /** * @generated from message netclode.v1.SessionDeletedResponse @@ -1129,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, 31); + messageDesc(file_netclode_v1_client, 32); /** * @generated from message netclode.v1.SessionsDeletedAllResponse @@ -1151,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, 32); + messageDesc(file_netclode_v1_client, 33); /** * @generated from message netclode.v1.SessionListResponse @@ -1173,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, 33); + messageDesc(file_netclode_v1_client, 34); /** * @generated from message netclode.v1.SessionStateResponse @@ -1223,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, 34); + messageDesc(file_netclode_v1_client, 35); /** * @generated from message netclode.v1.SyncResponse @@ -1250,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, 35); + messageDesc(file_netclode_v1_client, 36); /** * StreamEntryResponse wraps a StreamEntry for real-time push notifications. @@ -1275,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, 36); + messageDesc(file_netclode_v1_client, 37); /** * @generated from message netclode.v1.PortExposedResponse @@ -1307,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, 37); + 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 @@ -1329,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, 38); + messageDesc(file_netclode_v1_client, 40); /** * @generated from message netclode.v1.GitStatusResponse @@ -1356,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, 39); + messageDesc(file_netclode_v1_client, 41); /** * @generated from message netclode.v1.GitDiffResponse @@ -1383,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, 40); + messageDesc(file_netclode_v1_client, 42); /** * ErrorResponse is the unified error type for all error conditions. @@ -1412,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, 41); + messageDesc(file_netclode_v1_client, 43); /** * @generated from message netclode.v1.ModelsResponse @@ -1443,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, 42); + messageDesc(file_netclode_v1_client, 44); /** * @generated from message netclode.v1.CopilotStatusResponse @@ -1474,7 +1540,7 @@ 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, 43); + messageDesc(file_netclode_v1_client, 45); /** * @generated from message netclode.v1.CodexAuthStartedResponse @@ -1516,7 +1582,7 @@ export type CodexAuthStartedResponse = Message<"netclode.v1.CodexAuthStartedResp * Use `create(CodexAuthStartedResponseSchema)` to create a new message. */ export const CodexAuthStartedResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_netclode_v1_client, 44); + messageDesc(file_netclode_v1_client, 46); /** * @generated from message netclode.v1.CodexAuthStatusResponse @@ -1553,7 +1619,7 @@ export type CodexAuthStatusResponse = Message<"netclode.v1.CodexAuthStatusRespon * Use `create(CodexAuthStatusResponseSchema)` to create a new message. */ export const CodexAuthStatusResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_netclode_v1_client, 45); + messageDesc(file_netclode_v1_client, 47); /** * @generated from message netclode.v1.CodexAuthLoggedOutResponse @@ -1570,7 +1636,7 @@ export type CodexAuthLoggedOutResponse = Message<"netclode.v1.CodexAuthLoggedOut * Use `create(CodexAuthLoggedOutResponseSchema)` to create a new message. */ export const CodexAuthLoggedOutResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_netclode_v1_client, 46); + messageDesc(file_netclode_v1_client, 48); /** * SnapshotCreatedResponse is pushed to clients when an auto-snapshot is created after a turn. @@ -1594,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, 47); + messageDesc(file_netclode_v1_client, 49); /** * @generated from message netclode.v1.SnapshotListResponse @@ -1623,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, 48); + messageDesc(file_netclode_v1_client, 50); /** * SnapshotRestoredResponse is sent after workspace and messages are restored. @@ -1659,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, 49); + messageDesc(file_netclode_v1_client, 51); /** * RepoAccessUpdatedResponse is sent after repo access level is updated. @@ -1690,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, 50); + messageDesc(file_netclode_v1_client, 52); /** * ResourceLimitsResponse contains the maximum sandbox resource allocation. @@ -1738,7 +1804,7 @@ 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, 51); + messageDesc(file_netclode_v1_client, 53); /** * @generated from enum netclode.v1.CodexAuthState 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/control-plane/gen/netclode/v1/client.pb.go b/services/control-plane/gen/netclode/v1/client.pb.go index 3378ed3f..21b19550 100644 --- a/services/control-plane/gen/netclode/v1/client.pb.go +++ b/services/control-plane/gen/netclode/v1/client.pb.go @@ -107,6 +107,7 @@ type ClientMessage struct { // *ClientMessage_CodexAuthStart // *ClientMessage_CodexAuthStatus // *ClientMessage_CodexAuthLogout + // *ClientMessage_UnexposePort Message isClientMessage_Message `protobuf_oneof:"message"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache @@ -374,6 +375,15 @@ func (x *ClientMessage) GetCodexAuthLogout() *CodexAuthLogoutRequest { 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() } @@ -482,6 +492,10 @@ 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() {} @@ -532,6 +546,8 @@ 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"` @@ -560,6 +576,7 @@ type ServerMessage struct { // *ServerMessage_CodexAuthStarted // *ServerMessage_CodexAuthStatus // *ServerMessage_CodexAuthLoggedOut + // *ServerMessage_PortUnexposed Message isServerMessage_Message `protobuf_oneof:"message"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache @@ -809,6 +826,15 @@ func (x *ServerMessage) GetCodexAuthLoggedOut() *CodexAuthLoggedOutResponse { 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() } @@ -911,6 +937,10 @@ 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() {} @@ -957,6 +987,8 @@ 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"` @@ -1810,6 +1842,66 @@ func (x *ExposePortRequest) GetPort() int32 { return 0 } +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 *UnexposePortRequest) Reset() { + *x = UnexposePortRequest{} + mi := &file_netclode_v1_client_proto_msgTypes[16] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UnexposePortRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UnexposePortRequest) ProtoMessage() {} + +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 { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UnexposePortRequest.ProtoReflect.Descriptor instead. +func (*UnexposePortRequest) Descriptor() ([]byte, []int) { + return file_netclode_v1_client_proto_rawDescGZIP(), []int{16} +} + +func (x *UnexposePortRequest) GetRequestId() string { + if x != nil && x.RequestId != nil { + return *x.RequestId + } + return "" +} + +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"` @@ -1819,7 +1911,7 @@ type SyncRequest struct { func (x *SyncRequest) Reset() { *x = SyncRequest{} - mi := &file_netclode_v1_client_proto_msgTypes[16] + mi := &file_netclode_v1_client_proto_msgTypes[17] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1831,7 +1923,7 @@ func (x *SyncRequest) String() string { func (*SyncRequest) ProtoMessage() {} func (x *SyncRequest) ProtoReflect() protoreflect.Message { - mi := &file_netclode_v1_client_proto_msgTypes[16] + mi := &file_netclode_v1_client_proto_msgTypes[17] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1844,7 +1936,7 @@ func (x *SyncRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use SyncRequest.ProtoReflect.Descriptor instead. func (*SyncRequest) Descriptor() ([]byte, []int) { - return file_netclode_v1_client_proto_rawDescGZIP(), []int{16} + return file_netclode_v1_client_proto_rawDescGZIP(), []int{17} } func (x *SyncRequest) GetRequestId() string { @@ -1863,7 +1955,7 @@ type ListGitHubReposRequest struct { func (x *ListGitHubReposRequest) Reset() { *x = ListGitHubReposRequest{} - mi := &file_netclode_v1_client_proto_msgTypes[17] + mi := &file_netclode_v1_client_proto_msgTypes[18] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1875,7 +1967,7 @@ func (x *ListGitHubReposRequest) String() string { func (*ListGitHubReposRequest) ProtoMessage() {} func (x *ListGitHubReposRequest) ProtoReflect() protoreflect.Message { - mi := &file_netclode_v1_client_proto_msgTypes[17] + mi := &file_netclode_v1_client_proto_msgTypes[18] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1888,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{17} + return file_netclode_v1_client_proto_rawDescGZIP(), []int{18} } func (x *ListGitHubReposRequest) GetRequestId() string { @@ -1908,7 +2000,7 @@ type GitStatusRequest struct { func (x *GitStatusRequest) Reset() { *x = GitStatusRequest{} - mi := &file_netclode_v1_client_proto_msgTypes[18] + mi := &file_netclode_v1_client_proto_msgTypes[19] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1920,7 +2012,7 @@ func (x *GitStatusRequest) String() string { func (*GitStatusRequest) ProtoMessage() {} func (x *GitStatusRequest) ProtoReflect() protoreflect.Message { - mi := &file_netclode_v1_client_proto_msgTypes[18] + mi := &file_netclode_v1_client_proto_msgTypes[19] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1933,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{18} + return file_netclode_v1_client_proto_rawDescGZIP(), []int{19} } func (x *GitStatusRequest) GetRequestId() string { @@ -1961,7 +2053,7 @@ type GitDiffRequest struct { func (x *GitDiffRequest) Reset() { *x = GitDiffRequest{} - mi := &file_netclode_v1_client_proto_msgTypes[19] + mi := &file_netclode_v1_client_proto_msgTypes[20] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1973,7 +2065,7 @@ func (x *GitDiffRequest) String() string { func (*GitDiffRequest) ProtoMessage() {} func (x *GitDiffRequest) ProtoReflect() protoreflect.Message { - mi := &file_netclode_v1_client_proto_msgTypes[19] + mi := &file_netclode_v1_client_proto_msgTypes[20] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1986,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{19} + return file_netclode_v1_client_proto_rawDescGZIP(), []int{20} } func (x *GitDiffRequest) GetRequestId() string { @@ -2022,7 +2114,7 @@ type ListModelsRequest struct { func (x *ListModelsRequest) Reset() { *x = ListModelsRequest{} - mi := &file_netclode_v1_client_proto_msgTypes[20] + mi := &file_netclode_v1_client_proto_msgTypes[21] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2034,7 +2126,7 @@ func (x *ListModelsRequest) String() string { func (*ListModelsRequest) ProtoMessage() {} func (x *ListModelsRequest) ProtoReflect() protoreflect.Message { - mi := &file_netclode_v1_client_proto_msgTypes[20] + mi := &file_netclode_v1_client_proto_msgTypes[21] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2047,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{20} + return file_netclode_v1_client_proto_rawDescGZIP(), []int{21} } func (x *ListModelsRequest) GetRequestId() string { @@ -2087,7 +2179,7 @@ type GetCopilotStatusRequest struct { func (x *GetCopilotStatusRequest) Reset() { *x = GetCopilotStatusRequest{} - mi := &file_netclode_v1_client_proto_msgTypes[21] + mi := &file_netclode_v1_client_proto_msgTypes[22] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2099,7 +2191,7 @@ func (x *GetCopilotStatusRequest) String() string { func (*GetCopilotStatusRequest) ProtoMessage() {} func (x *GetCopilotStatusRequest) ProtoReflect() protoreflect.Message { - mi := &file_netclode_v1_client_proto_msgTypes[21] + mi := &file_netclode_v1_client_proto_msgTypes[22] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2112,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{21} + return file_netclode_v1_client_proto_rawDescGZIP(), []int{22} } func (x *GetCopilotStatusRequest) GetRequestId() string { @@ -2132,7 +2224,7 @@ type ListSnapshotsRequest struct { func (x *ListSnapshotsRequest) Reset() { *x = ListSnapshotsRequest{} - mi := &file_netclode_v1_client_proto_msgTypes[22] + mi := &file_netclode_v1_client_proto_msgTypes[23] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2144,7 +2236,7 @@ func (x *ListSnapshotsRequest) String() string { func (*ListSnapshotsRequest) ProtoMessage() {} func (x *ListSnapshotsRequest) ProtoReflect() protoreflect.Message { - mi := &file_netclode_v1_client_proto_msgTypes[22] + mi := &file_netclode_v1_client_proto_msgTypes[23] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2157,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{22} + return file_netclode_v1_client_proto_rawDescGZIP(), []int{23} } func (x *ListSnapshotsRequest) GetRequestId() string { @@ -2185,7 +2277,7 @@ type RestoreSnapshotRequest struct { func (x *RestoreSnapshotRequest) Reset() { *x = RestoreSnapshotRequest{} - mi := &file_netclode_v1_client_proto_msgTypes[23] + mi := &file_netclode_v1_client_proto_msgTypes[24] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2197,7 +2289,7 @@ func (x *RestoreSnapshotRequest) String() string { func (*RestoreSnapshotRequest) ProtoMessage() {} func (x *RestoreSnapshotRequest) ProtoReflect() protoreflect.Message { - mi := &file_netclode_v1_client_proto_msgTypes[23] + mi := &file_netclode_v1_client_proto_msgTypes[24] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2210,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{23} + return file_netclode_v1_client_proto_rawDescGZIP(), []int{24} } func (x *RestoreSnapshotRequest) GetRequestId() string { @@ -2245,7 +2337,7 @@ type UpdateRepoAccessRequest struct { func (x *UpdateRepoAccessRequest) Reset() { *x = UpdateRepoAccessRequest{} - mi := &file_netclode_v1_client_proto_msgTypes[24] + mi := &file_netclode_v1_client_proto_msgTypes[25] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2257,7 +2349,7 @@ func (x *UpdateRepoAccessRequest) String() string { func (*UpdateRepoAccessRequest) ProtoMessage() {} func (x *UpdateRepoAccessRequest) ProtoReflect() protoreflect.Message { - mi := &file_netclode_v1_client_proto_msgTypes[24] + mi := &file_netclode_v1_client_proto_msgTypes[25] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2270,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{24} + return file_netclode_v1_client_proto_rawDescGZIP(), []int{25} } func (x *UpdateRepoAccessRequest) GetRequestId() string { @@ -2303,7 +2395,7 @@ type GetResourceLimitsRequest struct { func (x *GetResourceLimitsRequest) Reset() { *x = GetResourceLimitsRequest{} - mi := &file_netclode_v1_client_proto_msgTypes[25] + mi := &file_netclode_v1_client_proto_msgTypes[26] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2315,7 +2407,7 @@ func (x *GetResourceLimitsRequest) String() string { func (*GetResourceLimitsRequest) ProtoMessage() {} func (x *GetResourceLimitsRequest) ProtoReflect() protoreflect.Message { - mi := &file_netclode_v1_client_proto_msgTypes[25] + mi := &file_netclode_v1_client_proto_msgTypes[26] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2328,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{25} + return file_netclode_v1_client_proto_rawDescGZIP(), []int{26} } func (x *GetResourceLimitsRequest) GetRequestId() string { @@ -2347,7 +2439,7 @@ type CodexAuthStartRequest struct { func (x *CodexAuthStartRequest) Reset() { *x = CodexAuthStartRequest{} - mi := &file_netclode_v1_client_proto_msgTypes[26] + mi := &file_netclode_v1_client_proto_msgTypes[27] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2359,7 +2451,7 @@ func (x *CodexAuthStartRequest) String() string { func (*CodexAuthStartRequest) ProtoMessage() {} func (x *CodexAuthStartRequest) ProtoReflect() protoreflect.Message { - mi := &file_netclode_v1_client_proto_msgTypes[26] + mi := &file_netclode_v1_client_proto_msgTypes[27] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2372,7 +2464,7 @@ func (x *CodexAuthStartRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use CodexAuthStartRequest.ProtoReflect.Descriptor instead. func (*CodexAuthStartRequest) Descriptor() ([]byte, []int) { - return file_netclode_v1_client_proto_rawDescGZIP(), []int{26} + return file_netclode_v1_client_proto_rawDescGZIP(), []int{27} } func (x *CodexAuthStartRequest) GetRequestId() string { @@ -2391,7 +2483,7 @@ type CodexAuthStatusRequest struct { func (x *CodexAuthStatusRequest) Reset() { *x = CodexAuthStatusRequest{} - mi := &file_netclode_v1_client_proto_msgTypes[27] + mi := &file_netclode_v1_client_proto_msgTypes[28] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2403,7 +2495,7 @@ func (x *CodexAuthStatusRequest) String() string { func (*CodexAuthStatusRequest) ProtoMessage() {} func (x *CodexAuthStatusRequest) ProtoReflect() protoreflect.Message { - mi := &file_netclode_v1_client_proto_msgTypes[27] + mi := &file_netclode_v1_client_proto_msgTypes[28] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2416,7 +2508,7 @@ func (x *CodexAuthStatusRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use CodexAuthStatusRequest.ProtoReflect.Descriptor instead. func (*CodexAuthStatusRequest) Descriptor() ([]byte, []int) { - return file_netclode_v1_client_proto_rawDescGZIP(), []int{27} + return file_netclode_v1_client_proto_rawDescGZIP(), []int{28} } func (x *CodexAuthStatusRequest) GetRequestId() string { @@ -2435,7 +2527,7 @@ type CodexAuthLogoutRequest struct { func (x *CodexAuthLogoutRequest) Reset() { *x = CodexAuthLogoutRequest{} - mi := &file_netclode_v1_client_proto_msgTypes[28] + mi := &file_netclode_v1_client_proto_msgTypes[29] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2447,7 +2539,7 @@ func (x *CodexAuthLogoutRequest) String() string { func (*CodexAuthLogoutRequest) ProtoMessage() {} func (x *CodexAuthLogoutRequest) ProtoReflect() protoreflect.Message { - mi := &file_netclode_v1_client_proto_msgTypes[28] + mi := &file_netclode_v1_client_proto_msgTypes[29] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2460,7 +2552,7 @@ func (x *CodexAuthLogoutRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use CodexAuthLogoutRequest.ProtoReflect.Descriptor instead. func (*CodexAuthLogoutRequest) Descriptor() ([]byte, []int) { - return file_netclode_v1_client_proto_rawDescGZIP(), []int{28} + return file_netclode_v1_client_proto_rawDescGZIP(), []int{29} } func (x *CodexAuthLogoutRequest) GetRequestId() string { @@ -2480,7 +2572,7 @@ type SessionCreatedResponse struct { func (x *SessionCreatedResponse) Reset() { *x = SessionCreatedResponse{} - mi := &file_netclode_v1_client_proto_msgTypes[29] + mi := &file_netclode_v1_client_proto_msgTypes[30] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2492,7 +2584,7 @@ func (x *SessionCreatedResponse) String() string { func (*SessionCreatedResponse) ProtoMessage() {} func (x *SessionCreatedResponse) ProtoReflect() protoreflect.Message { - mi := &file_netclode_v1_client_proto_msgTypes[29] + mi := &file_netclode_v1_client_proto_msgTypes[30] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2505,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{29} + return file_netclode_v1_client_proto_rawDescGZIP(), []int{30} } func (x *SessionCreatedResponse) GetSession() *Session { @@ -2531,7 +2623,7 @@ type SessionUpdatedResponse struct { func (x *SessionUpdatedResponse) Reset() { *x = SessionUpdatedResponse{} - mi := &file_netclode_v1_client_proto_msgTypes[30] + mi := &file_netclode_v1_client_proto_msgTypes[31] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2543,7 +2635,7 @@ func (x *SessionUpdatedResponse) String() string { func (*SessionUpdatedResponse) ProtoMessage() {} func (x *SessionUpdatedResponse) ProtoReflect() protoreflect.Message { - mi := &file_netclode_v1_client_proto_msgTypes[30] + mi := &file_netclode_v1_client_proto_msgTypes[31] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2556,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{30} + return file_netclode_v1_client_proto_rawDescGZIP(), []int{31} } func (x *SessionUpdatedResponse) GetSession() *Session { @@ -2576,7 +2668,7 @@ type SessionDeletedResponse struct { func (x *SessionDeletedResponse) Reset() { *x = SessionDeletedResponse{} - mi := &file_netclode_v1_client_proto_msgTypes[31] + mi := &file_netclode_v1_client_proto_msgTypes[32] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2588,7 +2680,7 @@ func (x *SessionDeletedResponse) String() string { func (*SessionDeletedResponse) ProtoMessage() {} func (x *SessionDeletedResponse) ProtoReflect() protoreflect.Message { - mi := &file_netclode_v1_client_proto_msgTypes[31] + mi := &file_netclode_v1_client_proto_msgTypes[32] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2601,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{31} + return file_netclode_v1_client_proto_rawDescGZIP(), []int{32} } func (x *SessionDeletedResponse) GetSessionId() string { @@ -2628,7 +2720,7 @@ type SessionsDeletedAllResponse struct { func (x *SessionsDeletedAllResponse) Reset() { *x = SessionsDeletedAllResponse{} - mi := &file_netclode_v1_client_proto_msgTypes[32] + mi := &file_netclode_v1_client_proto_msgTypes[33] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2640,7 +2732,7 @@ func (x *SessionsDeletedAllResponse) String() string { func (*SessionsDeletedAllResponse) ProtoMessage() {} func (x *SessionsDeletedAllResponse) ProtoReflect() protoreflect.Message { - mi := &file_netclode_v1_client_proto_msgTypes[32] + mi := &file_netclode_v1_client_proto_msgTypes[33] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2653,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{32} + return file_netclode_v1_client_proto_rawDescGZIP(), []int{33} } func (x *SessionsDeletedAllResponse) GetDeletedIds() []string { @@ -2680,7 +2772,7 @@ type SessionListResponse struct { func (x *SessionListResponse) Reset() { *x = SessionListResponse{} - mi := &file_netclode_v1_client_proto_msgTypes[33] + mi := &file_netclode_v1_client_proto_msgTypes[34] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2692,7 +2784,7 @@ func (x *SessionListResponse) String() string { func (*SessionListResponse) ProtoMessage() {} func (x *SessionListResponse) ProtoReflect() protoreflect.Message { - mi := &file_netclode_v1_client_proto_msgTypes[33] + mi := &file_netclode_v1_client_proto_msgTypes[34] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2705,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{33} + return file_netclode_v1_client_proto_rawDescGZIP(), []int{34} } func (x *SessionListResponse) GetSessions() []*Session { @@ -2736,7 +2828,7 @@ type SessionStateResponse struct { func (x *SessionStateResponse) Reset() { *x = SessionStateResponse{} - mi := &file_netclode_v1_client_proto_msgTypes[34] + mi := &file_netclode_v1_client_proto_msgTypes[35] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2748,7 +2840,7 @@ func (x *SessionStateResponse) String() string { func (*SessionStateResponse) ProtoMessage() {} func (x *SessionStateResponse) ProtoReflect() protoreflect.Message { - mi := &file_netclode_v1_client_proto_msgTypes[34] + mi := &file_netclode_v1_client_proto_msgTypes[35] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2761,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{34} + return file_netclode_v1_client_proto_rawDescGZIP(), []int{35} } func (x *SessionStateResponse) GetSession() *Session { @@ -2817,7 +2909,7 @@ type SyncResponse struct { func (x *SyncResponse) Reset() { *x = SyncResponse{} - mi := &file_netclode_v1_client_proto_msgTypes[35] + mi := &file_netclode_v1_client_proto_msgTypes[36] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2829,7 +2921,7 @@ func (x *SyncResponse) String() string { func (*SyncResponse) ProtoMessage() {} func (x *SyncResponse) ProtoReflect() protoreflect.Message { - mi := &file_netclode_v1_client_proto_msgTypes[35] + mi := &file_netclode_v1_client_proto_msgTypes[36] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2842,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{35} + return file_netclode_v1_client_proto_rawDescGZIP(), []int{36} } func (x *SyncResponse) GetSessions() []*SessionSummary { @@ -2878,7 +2970,7 @@ type StreamEntryResponse struct { func (x *StreamEntryResponse) Reset() { *x = StreamEntryResponse{} - mi := &file_netclode_v1_client_proto_msgTypes[36] + mi := &file_netclode_v1_client_proto_msgTypes[37] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2890,7 +2982,7 @@ func (x *StreamEntryResponse) String() string { func (*StreamEntryResponse) ProtoMessage() {} func (x *StreamEntryResponse) ProtoReflect() protoreflect.Message { - mi := &file_netclode_v1_client_proto_msgTypes[36] + mi := &file_netclode_v1_client_proto_msgTypes[37] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2903,7 +2995,7 @@ func (x *StreamEntryResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use StreamEntryResponse.ProtoReflect.Descriptor instead. func (*StreamEntryResponse) Descriptor() ([]byte, []int) { - return file_netclode_v1_client_proto_rawDescGZIP(), []int{36} + return file_netclode_v1_client_proto_rawDescGZIP(), []int{37} } func (x *StreamEntryResponse) GetSessionId() string { @@ -2932,7 +3024,7 @@ type PortExposedResponse struct { func (x *PortExposedResponse) Reset() { *x = PortExposedResponse{} - mi := &file_netclode_v1_client_proto_msgTypes[37] + mi := &file_netclode_v1_client_proto_msgTypes[38] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2944,7 +3036,7 @@ func (x *PortExposedResponse) String() string { func (*PortExposedResponse) ProtoMessage() {} func (x *PortExposedResponse) ProtoReflect() protoreflect.Message { - mi := &file_netclode_v1_client_proto_msgTypes[37] + mi := &file_netclode_v1_client_proto_msgTypes[38] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2957,7 +3049,7 @@ func (x *PortExposedResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use PortExposedResponse.ProtoReflect.Descriptor instead. func (*PortExposedResponse) Descriptor() ([]byte, []int) { - return file_netclode_v1_client_proto_rawDescGZIP(), []int{37} + return file_netclode_v1_client_proto_rawDescGZIP(), []int{38} } func (x *PortExposedResponse) GetSessionId() string { @@ -2988,6 +3080,66 @@ func (x *PortExposedResponse) GetRequestId() string { return "" } +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"` + 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 *PortUnexposedResponse) Reset() { + *x = PortUnexposedResponse{} + mi := &file_netclode_v1_client_proto_msgTypes[39] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *PortUnexposedResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PortUnexposedResponse) ProtoMessage() {} + +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 { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PortUnexposedResponse.ProtoReflect.Descriptor instead. +func (*PortUnexposedResponse) Descriptor() ([]byte, []int) { + return file_netclode_v1_client_proto_rawDescGZIP(), []int{39} +} + +func (x *PortUnexposedResponse) GetSessionId() string { + if x != nil { + return x.SessionId + } + return "" +} + +func (x *PortUnexposedResponse) GetPort() int32 { + if x != nil { + return x.Port + } + return 0 +} + +func (x *PortUnexposedResponse) GetRequestId() string { + if x != nil && x.RequestId != nil { + return *x.RequestId + } + return "" +} + type GitHubReposResponse struct { state protoimpl.MessageState `protogen:"open.v1"` Repos []*GitHubRepo `protobuf:"bytes,1,rep,name=repos,proto3" json:"repos,omitempty"` @@ -2998,7 +3150,7 @@ type GitHubReposResponse struct { func (x *GitHubReposResponse) Reset() { *x = GitHubReposResponse{} - mi := &file_netclode_v1_client_proto_msgTypes[38] + mi := &file_netclode_v1_client_proto_msgTypes[40] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3010,7 +3162,7 @@ func (x *GitHubReposResponse) String() string { func (*GitHubReposResponse) ProtoMessage() {} func (x *GitHubReposResponse) ProtoReflect() protoreflect.Message { - mi := &file_netclode_v1_client_proto_msgTypes[38] + mi := &file_netclode_v1_client_proto_msgTypes[40] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3023,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{38} + return file_netclode_v1_client_proto_rawDescGZIP(), []int{40} } func (x *GitHubReposResponse) GetRepos() []*GitHubRepo { @@ -3051,7 +3203,7 @@ type GitStatusResponse struct { func (x *GitStatusResponse) Reset() { *x = GitStatusResponse{} - mi := &file_netclode_v1_client_proto_msgTypes[39] + mi := &file_netclode_v1_client_proto_msgTypes[41] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3063,7 +3215,7 @@ func (x *GitStatusResponse) String() string { func (*GitStatusResponse) ProtoMessage() {} func (x *GitStatusResponse) ProtoReflect() protoreflect.Message { - mi := &file_netclode_v1_client_proto_msgTypes[39] + mi := &file_netclode_v1_client_proto_msgTypes[41] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3076,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{39} + return file_netclode_v1_client_proto_rawDescGZIP(), []int{41} } func (x *GitStatusResponse) GetSessionId() string { @@ -3111,7 +3263,7 @@ type GitDiffResponse struct { func (x *GitDiffResponse) Reset() { *x = GitDiffResponse{} - mi := &file_netclode_v1_client_proto_msgTypes[40] + mi := &file_netclode_v1_client_proto_msgTypes[42] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3123,7 +3275,7 @@ func (x *GitDiffResponse) String() string { func (*GitDiffResponse) ProtoMessage() {} func (x *GitDiffResponse) ProtoReflect() protoreflect.Message { - mi := &file_netclode_v1_client_proto_msgTypes[40] + mi := &file_netclode_v1_client_proto_msgTypes[42] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3136,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{40} + return file_netclode_v1_client_proto_rawDescGZIP(), []int{42} } func (x *GitDiffResponse) GetSessionId() string { @@ -3172,7 +3324,7 @@ type ErrorResponse struct { func (x *ErrorResponse) Reset() { *x = ErrorResponse{} - mi := &file_netclode_v1_client_proto_msgTypes[41] + mi := &file_netclode_v1_client_proto_msgTypes[43] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3184,7 +3336,7 @@ func (x *ErrorResponse) String() string { func (*ErrorResponse) ProtoMessage() {} func (x *ErrorResponse) ProtoReflect() protoreflect.Message { - mi := &file_netclode_v1_client_proto_msgTypes[41] + mi := &file_netclode_v1_client_proto_msgTypes[43] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3197,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{41} + return file_netclode_v1_client_proto_rawDescGZIP(), []int{43} } func (x *ErrorResponse) GetError() *Error { @@ -3225,7 +3377,7 @@ type ModelsResponse struct { func (x *ModelsResponse) Reset() { *x = ModelsResponse{} - mi := &file_netclode_v1_client_proto_msgTypes[42] + mi := &file_netclode_v1_client_proto_msgTypes[44] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3237,7 +3389,7 @@ func (x *ModelsResponse) String() string { func (*ModelsResponse) ProtoMessage() {} func (x *ModelsResponse) ProtoReflect() protoreflect.Message { - mi := &file_netclode_v1_client_proto_msgTypes[42] + mi := &file_netclode_v1_client_proto_msgTypes[44] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3250,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{42} + return file_netclode_v1_client_proto_rawDescGZIP(), []int{44} } func (x *ModelsResponse) GetModels() []*ModelInfo { @@ -3285,7 +3437,7 @@ type CopilotStatusResponse struct { func (x *CopilotStatusResponse) Reset() { *x = CopilotStatusResponse{} - mi := &file_netclode_v1_client_proto_msgTypes[43] + mi := &file_netclode_v1_client_proto_msgTypes[45] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3297,7 +3449,7 @@ func (x *CopilotStatusResponse) String() string { func (*CopilotStatusResponse) ProtoMessage() {} func (x *CopilotStatusResponse) ProtoReflect() protoreflect.Message { - mi := &file_netclode_v1_client_proto_msgTypes[43] + mi := &file_netclode_v1_client_proto_msgTypes[45] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3310,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{43} + return file_netclode_v1_client_proto_rawDescGZIP(), []int{45} } func (x *CopilotStatusResponse) GetAuth() *CopilotAuthStatus { @@ -3348,7 +3500,7 @@ type CodexAuthStartedResponse struct { func (x *CodexAuthStartedResponse) Reset() { *x = CodexAuthStartedResponse{} - mi := &file_netclode_v1_client_proto_msgTypes[44] + mi := &file_netclode_v1_client_proto_msgTypes[46] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3360,7 +3512,7 @@ func (x *CodexAuthStartedResponse) String() string { func (*CodexAuthStartedResponse) ProtoMessage() {} func (x *CodexAuthStartedResponse) ProtoReflect() protoreflect.Message { - mi := &file_netclode_v1_client_proto_msgTypes[44] + mi := &file_netclode_v1_client_proto_msgTypes[46] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3373,7 +3525,7 @@ func (x *CodexAuthStartedResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use CodexAuthStartedResponse.ProtoReflect.Descriptor instead. func (*CodexAuthStartedResponse) Descriptor() ([]byte, []int) { - return file_netclode_v1_client_proto_rawDescGZIP(), []int{44} + return file_netclode_v1_client_proto_rawDescGZIP(), []int{46} } func (x *CodexAuthStartedResponse) GetVerificationUri() string { @@ -3431,7 +3583,7 @@ type CodexAuthStatusResponse struct { func (x *CodexAuthStatusResponse) Reset() { *x = CodexAuthStatusResponse{} - mi := &file_netclode_v1_client_proto_msgTypes[45] + mi := &file_netclode_v1_client_proto_msgTypes[47] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3443,7 +3595,7 @@ func (x *CodexAuthStatusResponse) String() string { func (*CodexAuthStatusResponse) ProtoMessage() {} func (x *CodexAuthStatusResponse) ProtoReflect() protoreflect.Message { - mi := &file_netclode_v1_client_proto_msgTypes[45] + mi := &file_netclode_v1_client_proto_msgTypes[47] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3456,7 +3608,7 @@ func (x *CodexAuthStatusResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use CodexAuthStatusResponse.ProtoReflect.Descriptor instead. func (*CodexAuthStatusResponse) Descriptor() ([]byte, []int) { - return file_netclode_v1_client_proto_rawDescGZIP(), []int{45} + return file_netclode_v1_client_proto_rawDescGZIP(), []int{47} } func (x *CodexAuthStatusResponse) GetState() CodexAuthState { @@ -3503,7 +3655,7 @@ type CodexAuthLoggedOutResponse struct { func (x *CodexAuthLoggedOutResponse) Reset() { *x = CodexAuthLoggedOutResponse{} - mi := &file_netclode_v1_client_proto_msgTypes[46] + mi := &file_netclode_v1_client_proto_msgTypes[48] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3515,7 +3667,7 @@ func (x *CodexAuthLoggedOutResponse) String() string { func (*CodexAuthLoggedOutResponse) ProtoMessage() {} func (x *CodexAuthLoggedOutResponse) ProtoReflect() protoreflect.Message { - mi := &file_netclode_v1_client_proto_msgTypes[46] + mi := &file_netclode_v1_client_proto_msgTypes[48] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3528,7 +3680,7 @@ func (x *CodexAuthLoggedOutResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use CodexAuthLoggedOutResponse.ProtoReflect.Descriptor instead. func (*CodexAuthLoggedOutResponse) Descriptor() ([]byte, []int) { - return file_netclode_v1_client_proto_rawDescGZIP(), []int{46} + return file_netclode_v1_client_proto_rawDescGZIP(), []int{48} } func (x *CodexAuthLoggedOutResponse) GetRequestId() string { @@ -3549,7 +3701,7 @@ type SnapshotCreatedResponse struct { func (x *SnapshotCreatedResponse) Reset() { *x = SnapshotCreatedResponse{} - mi := &file_netclode_v1_client_proto_msgTypes[47] + mi := &file_netclode_v1_client_proto_msgTypes[49] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3561,7 +3713,7 @@ func (x *SnapshotCreatedResponse) String() string { func (*SnapshotCreatedResponse) ProtoMessage() {} func (x *SnapshotCreatedResponse) ProtoReflect() protoreflect.Message { - mi := &file_netclode_v1_client_proto_msgTypes[47] + mi := &file_netclode_v1_client_proto_msgTypes[49] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3574,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{47} + return file_netclode_v1_client_proto_rawDescGZIP(), []int{49} } func (x *SnapshotCreatedResponse) GetSessionId() string { @@ -3602,7 +3754,7 @@ type SnapshotListResponse struct { func (x *SnapshotListResponse) Reset() { *x = SnapshotListResponse{} - mi := &file_netclode_v1_client_proto_msgTypes[48] + mi := &file_netclode_v1_client_proto_msgTypes[50] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3614,7 +3766,7 @@ func (x *SnapshotListResponse) String() string { func (*SnapshotListResponse) ProtoMessage() {} func (x *SnapshotListResponse) ProtoReflect() protoreflect.Message { - mi := &file_netclode_v1_client_proto_msgTypes[48] + mi := &file_netclode_v1_client_proto_msgTypes[50] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3627,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{48} + return file_netclode_v1_client_proto_rawDescGZIP(), []int{50} } func (x *SnapshotListResponse) GetSessionId() string { @@ -3664,7 +3816,7 @@ type SnapshotRestoredResponse struct { func (x *SnapshotRestoredResponse) Reset() { *x = SnapshotRestoredResponse{} - mi := &file_netclode_v1_client_proto_msgTypes[49] + mi := &file_netclode_v1_client_proto_msgTypes[51] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3676,7 +3828,7 @@ func (x *SnapshotRestoredResponse) String() string { func (*SnapshotRestoredResponse) ProtoMessage() {} func (x *SnapshotRestoredResponse) ProtoReflect() protoreflect.Message { - mi := &file_netclode_v1_client_proto_msgTypes[49] + mi := &file_netclode_v1_client_proto_msgTypes[51] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3689,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{49} + return file_netclode_v1_client_proto_rawDescGZIP(), []int{51} } func (x *SnapshotRestoredResponse) GetSessionId() string { @@ -3732,7 +3884,7 @@ type RepoAccessUpdatedResponse struct { func (x *RepoAccessUpdatedResponse) Reset() { *x = RepoAccessUpdatedResponse{} - mi := &file_netclode_v1_client_proto_msgTypes[50] + mi := &file_netclode_v1_client_proto_msgTypes[52] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3744,7 +3896,7 @@ func (x *RepoAccessUpdatedResponse) String() string { func (*RepoAccessUpdatedResponse) ProtoMessage() {} func (x *RepoAccessUpdatedResponse) ProtoReflect() protoreflect.Message { - mi := &file_netclode_v1_client_proto_msgTypes[50] + mi := &file_netclode_v1_client_proto_msgTypes[52] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3757,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{50} + return file_netclode_v1_client_proto_rawDescGZIP(), []int{52} } func (x *RepoAccessUpdatedResponse) GetSessionId() string { @@ -3796,7 +3948,7 @@ type ResourceLimitsResponse struct { func (x *ResourceLimitsResponse) Reset() { *x = ResourceLimitsResponse{} - mi := &file_netclode_v1_client_proto_msgTypes[51] + mi := &file_netclode_v1_client_proto_msgTypes[53] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3808,7 +3960,7 @@ func (x *ResourceLimitsResponse) String() string { func (*ResourceLimitsResponse) ProtoMessage() {} func (x *ResourceLimitsResponse) ProtoReflect() protoreflect.Message { - mi := &file_netclode_v1_client_proto_msgTypes[51] + mi := &file_netclode_v1_client_proto_msgTypes[53] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3821,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{51} + return file_netclode_v1_client_proto_rawDescGZIP(), []int{53} } func (x *ResourceLimitsResponse) GetMaxVcpus() int32 { @@ -3863,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\"\xfb\x0e\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" + @@ -3894,8 +4046,9 @@ const file_netclode_v1_client_proto_rawDesc = "" + "\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\x0fcodexAuthLogoutB\t\n" + - "\amessage\"\xe9\r\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" + @@ -3920,7 +4073,8 @@ const file_netclode_v1_client_proto_rawDesc = "" + "\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\x12codexAuthLoggedOutB\t\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\"\xc4\x01\n" + @@ -4027,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" + @@ -4156,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" + @@ -4282,7 +4450,7 @@ func file_netclode_v1_client_proto_rawDescGZIP() []byte { } var file_netclode_v1_client_proto_enumTypes = make([]protoimpl.EnumInfo, 1) -var file_netclode_v1_client_proto_msgTypes = make([]protoimpl.MessageInfo, 52) +var file_netclode_v1_client_proto_msgTypes = make([]protoimpl.MessageInfo, 54) var file_netclode_v1_client_proto_goTypes = []any{ (CodexAuthState)(0), // 0: netclode.v1.CodexAuthState (*ClientMessage)(nil), // 1: netclode.v1.ClientMessage @@ -4301,58 +4469,60 @@ var file_netclode_v1_client_proto_goTypes = []any{ (*TerminalInputRequest)(nil), // 14: netclode.v1.TerminalInputRequest (*TerminalResizeRequest)(nil), // 15: netclode.v1.TerminalResizeRequest (*ExposePortRequest)(nil), // 16: netclode.v1.ExposePortRequest - (*SyncRequest)(nil), // 17: netclode.v1.SyncRequest - (*ListGitHubReposRequest)(nil), // 18: netclode.v1.ListGitHubReposRequest - (*GitStatusRequest)(nil), // 19: netclode.v1.GitStatusRequest - (*GitDiffRequest)(nil), // 20: netclode.v1.GitDiffRequest - (*ListModelsRequest)(nil), // 21: netclode.v1.ListModelsRequest - (*GetCopilotStatusRequest)(nil), // 22: netclode.v1.GetCopilotStatusRequest - (*ListSnapshotsRequest)(nil), // 23: netclode.v1.ListSnapshotsRequest - (*RestoreSnapshotRequest)(nil), // 24: netclode.v1.RestoreSnapshotRequest - (*UpdateRepoAccessRequest)(nil), // 25: netclode.v1.UpdateRepoAccessRequest - (*GetResourceLimitsRequest)(nil), // 26: netclode.v1.GetResourceLimitsRequest - (*CodexAuthStartRequest)(nil), // 27: netclode.v1.CodexAuthStartRequest - (*CodexAuthStatusRequest)(nil), // 28: netclode.v1.CodexAuthStatusRequest - (*CodexAuthLogoutRequest)(nil), // 29: netclode.v1.CodexAuthLogoutRequest - (*SessionCreatedResponse)(nil), // 30: netclode.v1.SessionCreatedResponse - (*SessionUpdatedResponse)(nil), // 31: netclode.v1.SessionUpdatedResponse - (*SessionDeletedResponse)(nil), // 32: netclode.v1.SessionDeletedResponse - (*SessionsDeletedAllResponse)(nil), // 33: netclode.v1.SessionsDeletedAllResponse - (*SessionListResponse)(nil), // 34: netclode.v1.SessionListResponse - (*SessionStateResponse)(nil), // 35: netclode.v1.SessionStateResponse - (*SyncResponse)(nil), // 36: netclode.v1.SyncResponse - (*StreamEntryResponse)(nil), // 37: netclode.v1.StreamEntryResponse - (*PortExposedResponse)(nil), // 38: netclode.v1.PortExposedResponse - (*GitHubReposResponse)(nil), // 39: netclode.v1.GitHubReposResponse - (*GitStatusResponse)(nil), // 40: netclode.v1.GitStatusResponse - (*GitDiffResponse)(nil), // 41: netclode.v1.GitDiffResponse - (*ErrorResponse)(nil), // 42: netclode.v1.ErrorResponse - (*ModelsResponse)(nil), // 43: netclode.v1.ModelsResponse - (*CopilotStatusResponse)(nil), // 44: netclode.v1.CopilotStatusResponse - (*CodexAuthStartedResponse)(nil), // 45: netclode.v1.CodexAuthStartedResponse - (*CodexAuthStatusResponse)(nil), // 46: netclode.v1.CodexAuthStatusResponse - (*CodexAuthLoggedOutResponse)(nil), // 47: netclode.v1.CodexAuthLoggedOutResponse - (*SnapshotCreatedResponse)(nil), // 48: netclode.v1.SnapshotCreatedResponse - (*SnapshotListResponse)(nil), // 49: netclode.v1.SnapshotListResponse - (*SnapshotRestoredResponse)(nil), // 50: netclode.v1.SnapshotRestoredResponse - (*RepoAccessUpdatedResponse)(nil), // 51: netclode.v1.RepoAccessUpdatedResponse - (*ResourceLimitsResponse)(nil), // 52: netclode.v1.ResourceLimitsResponse - (*timestamppb.Timestamp)(nil), // 53: google.protobuf.Timestamp - (RepoAccess)(0), // 54: netclode.v1.RepoAccess - (SdkType)(0), // 55: netclode.v1.SdkType - (CopilotBackend)(0), // 56: netclode.v1.CopilotBackend - (*SandboxResources)(nil), // 57: netclode.v1.SandboxResources - (*Session)(nil), // 58: netclode.v1.Session - (*StreamEntry)(nil), // 59: netclode.v1.StreamEntry - (*InProgressState)(nil), // 60: netclode.v1.InProgressState - (*SessionSummary)(nil), // 61: netclode.v1.SessionSummary - (*GitHubRepo)(nil), // 62: netclode.v1.GitHubRepo - (*GitFileChange)(nil), // 63: netclode.v1.GitFileChange - (*Error)(nil), // 64: netclode.v1.Error - (*ModelInfo)(nil), // 65: netclode.v1.ModelInfo - (*CopilotAuthStatus)(nil), // 66: netclode.v1.CopilotAuthStatus - (*CopilotPremiumQuota)(nil), // 67: netclode.v1.CopilotPremiumQuota - (*Snapshot)(nil), // 68: netclode.v1.Snapshot + (*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{ 5, // 0: netclode.v1.ClientMessage.create_session:type_name -> netclode.v1.CreateSessionRequest @@ -4367,81 +4537,83 @@ var file_netclode_v1_client_proto_depIdxs = []int32{ 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 - 17, // 12: netclode.v1.ClientMessage.sync:type_name -> netclode.v1.SyncRequest - 18, // 13: netclode.v1.ClientMessage.list_github_repos:type_name -> netclode.v1.ListGitHubReposRequest - 19, // 14: netclode.v1.ClientMessage.git_status:type_name -> netclode.v1.GitStatusRequest - 20, // 15: netclode.v1.ClientMessage.git_diff:type_name -> netclode.v1.GitDiffRequest - 21, // 16: netclode.v1.ClientMessage.list_models:type_name -> netclode.v1.ListModelsRequest - 22, // 17: netclode.v1.ClientMessage.get_copilot_status:type_name -> netclode.v1.GetCopilotStatusRequest - 23, // 18: netclode.v1.ClientMessage.list_snapshots:type_name -> netclode.v1.ListSnapshotsRequest - 24, // 19: netclode.v1.ClientMessage.restore_snapshot:type_name -> netclode.v1.RestoreSnapshotRequest - 25, // 20: netclode.v1.ClientMessage.update_repo_access:type_name -> netclode.v1.UpdateRepoAccessRequest - 26, // 21: netclode.v1.ClientMessage.get_resource_limits:type_name -> netclode.v1.GetResourceLimitsRequest - 27, // 22: netclode.v1.ClientMessage.codex_auth_start:type_name -> netclode.v1.CodexAuthStartRequest - 28, // 23: netclode.v1.ClientMessage.codex_auth_status:type_name -> netclode.v1.CodexAuthStatusRequest - 29, // 24: netclode.v1.ClientMessage.codex_auth_logout:type_name -> netclode.v1.CodexAuthLogoutRequest - 30, // 25: netclode.v1.ServerMessage.session_created:type_name -> netclode.v1.SessionCreatedResponse - 31, // 26: netclode.v1.ServerMessage.session_updated:type_name -> netclode.v1.SessionUpdatedResponse - 32, // 27: netclode.v1.ServerMessage.session_deleted:type_name -> netclode.v1.SessionDeletedResponse - 33, // 28: netclode.v1.ServerMessage.sessions_deleted_all:type_name -> netclode.v1.SessionsDeletedAllResponse - 34, // 29: netclode.v1.ServerMessage.session_list:type_name -> netclode.v1.SessionListResponse - 35, // 30: netclode.v1.ServerMessage.session_state:type_name -> netclode.v1.SessionStateResponse - 36, // 31: netclode.v1.ServerMessage.sync_response:type_name -> netclode.v1.SyncResponse - 37, // 32: netclode.v1.ServerMessage.stream_entry:type_name -> netclode.v1.StreamEntryResponse - 38, // 33: netclode.v1.ServerMessage.port_exposed:type_name -> netclode.v1.PortExposedResponse - 39, // 34: netclode.v1.ServerMessage.github_repos:type_name -> netclode.v1.GitHubReposResponse - 40, // 35: netclode.v1.ServerMessage.git_status:type_name -> netclode.v1.GitStatusResponse - 41, // 36: netclode.v1.ServerMessage.git_diff:type_name -> netclode.v1.GitDiffResponse - 42, // 37: netclode.v1.ServerMessage.error:type_name -> netclode.v1.ErrorResponse - 43, // 38: netclode.v1.ServerMessage.models:type_name -> netclode.v1.ModelsResponse - 44, // 39: netclode.v1.ServerMessage.copilot_status:type_name -> netclode.v1.CopilotStatusResponse - 48, // 40: netclode.v1.ServerMessage.snapshot_created:type_name -> netclode.v1.SnapshotCreatedResponse - 49, // 41: netclode.v1.ServerMessage.snapshot_list:type_name -> netclode.v1.SnapshotListResponse - 50, // 42: netclode.v1.ServerMessage.snapshot_restored:type_name -> netclode.v1.SnapshotRestoredResponse - 51, // 43: netclode.v1.ServerMessage.repo_access_updated:type_name -> netclode.v1.RepoAccessUpdatedResponse - 52, // 44: netclode.v1.ServerMessage.resource_limits:type_name -> netclode.v1.ResourceLimitsResponse - 45, // 45: netclode.v1.ServerMessage.codex_auth_started:type_name -> netclode.v1.CodexAuthStartedResponse - 46, // 46: netclode.v1.ServerMessage.codex_auth_status:type_name -> netclode.v1.CodexAuthStatusResponse - 47, // 47: netclode.v1.ServerMessage.codex_auth_logged_out:type_name -> netclode.v1.CodexAuthLoggedOutResponse - 53, // 48: netclode.v1.CodexOAuthTokens.expires_at:type_name -> google.protobuf.Timestamp - 54, // 49: netclode.v1.CreateSessionRequest.repo_access:type_name -> netclode.v1.RepoAccess - 55, // 50: netclode.v1.CreateSessionRequest.sdk_type:type_name -> netclode.v1.SdkType - 56, // 51: netclode.v1.CreateSessionRequest.copilot_backend:type_name -> netclode.v1.CopilotBackend - 3, // 52: netclode.v1.CreateSessionRequest.network_config:type_name -> netclode.v1.NetworkConfig - 57, // 53: netclode.v1.CreateSessionRequest.resources:type_name -> netclode.v1.SandboxResources - 4, // 54: netclode.v1.CreateSessionRequest.codex_oauth_tokens:type_name -> netclode.v1.CodexOAuthTokens - 55, // 55: netclode.v1.ListModelsRequest.sdk_type:type_name -> netclode.v1.SdkType - 56, // 56: netclode.v1.ListModelsRequest.copilot_backend:type_name -> netclode.v1.CopilotBackend - 54, // 57: netclode.v1.UpdateRepoAccessRequest.repo_access:type_name -> netclode.v1.RepoAccess - 58, // 58: netclode.v1.SessionCreatedResponse.session:type_name -> netclode.v1.Session - 58, // 59: netclode.v1.SessionUpdatedResponse.session:type_name -> netclode.v1.Session - 58, // 60: netclode.v1.SessionListResponse.sessions:type_name -> netclode.v1.Session - 58, // 61: netclode.v1.SessionStateResponse.session:type_name -> netclode.v1.Session - 59, // 62: netclode.v1.SessionStateResponse.entries:type_name -> netclode.v1.StreamEntry - 60, // 63: netclode.v1.SessionStateResponse.in_progress:type_name -> netclode.v1.InProgressState - 61, // 64: netclode.v1.SyncResponse.sessions:type_name -> netclode.v1.SessionSummary - 53, // 65: netclode.v1.SyncResponse.server_time:type_name -> google.protobuf.Timestamp - 59, // 66: netclode.v1.StreamEntryResponse.entry:type_name -> netclode.v1.StreamEntry - 62, // 67: netclode.v1.GitHubReposResponse.repos:type_name -> netclode.v1.GitHubRepo - 63, // 68: netclode.v1.GitStatusResponse.files:type_name -> netclode.v1.GitFileChange - 64, // 69: netclode.v1.ErrorResponse.error:type_name -> netclode.v1.Error - 65, // 70: netclode.v1.ModelsResponse.models:type_name -> netclode.v1.ModelInfo - 55, // 71: netclode.v1.ModelsResponse.sdk_type:type_name -> netclode.v1.SdkType - 66, // 72: netclode.v1.CopilotStatusResponse.auth:type_name -> netclode.v1.CopilotAuthStatus - 67, // 73: netclode.v1.CopilotStatusResponse.quota:type_name -> netclode.v1.CopilotPremiumQuota - 53, // 74: netclode.v1.CodexAuthStartedResponse.expires_at:type_name -> google.protobuf.Timestamp - 0, // 75: netclode.v1.CodexAuthStatusResponse.state:type_name -> netclode.v1.CodexAuthState - 53, // 76: netclode.v1.CodexAuthStatusResponse.expires_at:type_name -> google.protobuf.Timestamp - 68, // 77: netclode.v1.SnapshotCreatedResponse.snapshot:type_name -> netclode.v1.Snapshot - 68, // 78: netclode.v1.SnapshotListResponse.snapshots:type_name -> netclode.v1.Snapshot - 54, // 79: netclode.v1.RepoAccessUpdatedResponse.repo_access:type_name -> netclode.v1.RepoAccess - 1, // 80: netclode.v1.ClientService.Connect:input_type -> netclode.v1.ClientMessage - 2, // 81: netclode.v1.ClientService.Connect:output_type -> netclode.v1.ServerMessage - 81, // [81:82] is the sub-list for method output_type - 80, // [80:81] is the sub-list for method input_type - 80, // [80:80] is the sub-list for extension type_name - 80, // [80:80] is the sub-list for extension extendee - 0, // [0:80] is the sub-list for field type_name + 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() } @@ -4477,6 +4649,7 @@ func file_netclode_v1_client_proto_init() { (*ClientMessage_CodexAuthStart)(nil), (*ClientMessage_CodexAuthStatus)(nil), (*ClientMessage_CodexAuthLogout)(nil), + (*ClientMessage_UnexposePort)(nil), } file_netclode_v1_client_proto_msgTypes[1].OneofWrappers = []any{ (*ServerMessage_SessionCreated)(nil), @@ -4502,6 +4675,7 @@ func file_netclode_v1_client_proto_init() { (*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{} @@ -4530,12 +4704,12 @@ func file_netclode_v1_client_proto_init() { 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[31].OneofWrappers = []any{} + file_netclode_v1_client_proto_msgTypes[30].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[37].OneofWrappers = []any{} + file_netclode_v1_client_proto_msgTypes[36].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{} @@ -4545,17 +4719,19 @@ func file_netclode_v1_client_proto_init() { 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[49].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: 1, - NumMessages: 52, + NumMessages: 54, NumExtensions: 0, NumServices: 1, }, 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_client.go b/services/control-plane/internal/api/connect_client.go index 75acdff6..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: @@ -654,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 { diff --git a/services/control-plane/internal/k8s/client.go b/services/control-plane/internal/k8s/client.go index 7ab1d78a..1c367909 100644 --- a/services/control-plane/internal/k8s/client.go +++ b/services/control-plane/internal/k8s/client.go @@ -42,6 +42,7 @@ 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 diff --git a/services/control-plane/internal/k8s/sandbox.go b/services/control-plane/internal/k8s/sandbox.go index e6d1dec6..b8d96351 100644 --- a/services/control-plane/internal/k8s/sandbox.go +++ b/services/control-plane/internal/k8s/sandbox.go @@ -975,6 +975,104 @@ 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) + + // 1. Remove port from the Tailscale service + svc, err := r.clientset.CoreV1().Services(r.namespace).Get(ctx, tailscaleSvcName, metav1.GetOptions{}) + if err != nil { + return fmt.Errorf("get tailscale service: %w", err) + } + + removedServicePort := false + 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 diff --git a/services/control-plane/internal/session/manager.go b/services/control-plane/internal/session/manager.go index 0bdeea68..2b7c1bee 100644 --- a/services/control-plane/internal/session/manager.go +++ b/services/control-plane/internal/session/manager.go @@ -1121,9 +1121,33 @@ 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 { + return 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}) @@ -1132,20 +1156,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) + } } } @@ -2915,7 +2944,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 aa7400a0..7a0da415 100644 --- a/services/control-plane/internal/session/manager_test.go +++ b/services/control-plane/internal/session/manager_test.go @@ -6,6 +6,7 @@ import ( "errors" "net/http" "net/http/httptest" + "strconv" "strings" "sync" "testing" @@ -16,6 +17,7 @@ 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" ) @@ -30,6 +32,7 @@ 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 previewHostname string @@ -39,6 +42,7 @@ type mockRuntime struct { 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), } @@ -137,6 +141,23 @@ 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 ports, exists := m.exposedPorts[sessionID]; exists { + delete(ports, port) + } return nil } @@ -263,6 +284,7 @@ func (m *mockRuntime) Close() { type mockStorage struct { mu sync.Mutex sessions map[string]*pb.Session + streams map[string][]storage.StreamEntryWithID oauth map[string]*storage.CodexOAuthSessionData oauthGlobal *storage.CodexOAuthSessionData } @@ -270,6 +292,7 @@ type mockStorage struct { func newMockStorage() *mockStorage { return &mockStorage{ sessions: make(map[string]*pb.Session), + streams: make(map[string][]storage.StreamEntryWithID), oauth: make(map[string]*storage.CodexOAuthSessionData), } } @@ -382,22 +405,97 @@ func (m *mockStorage) DeleteSession(ctx context.Context, id string) error { // 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 } @@ -1143,3 +1241,74 @@ func TestExposePort_FallsBackToShortHostnameOnLookupError(t *testing.T) { 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 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") + } +} From ce27671aac9ca725f25700b45e08fbbbfabc5d7a Mon Sep 17 00:00:00 2001 From: Bazyli Brzoska Date: Sat, 7 Feb 2026 00:22:25 -0800 Subject: [PATCH 13/21] fix(agent): reinitialize SDK adapters after stream reconnect ## Root cause The agent SDK factory caches adapter singletons by SDK type. On control-plane stream disconnect, the agent called `currentAdapter.shutdown()` which set internal clients (including Codex) to null, but the shutdown adapter remained cached. After reconnect, `createSDKAdapter()` returned the cached shutdown instance, causing prompt execution to fail with `Codex client not initialized`. ## What changed - Updated connect-client teardown to call `shutdownAllAdapters()` on disconnect. - Kept `currentAdapter = null` after shutdown. - This clears the factory cache and forces fresh adapter initialization on reconnect. ## Why this fixes the issue Clearing all cached adapters ensures reconnect paths construct a new initialized adapter instance instead of reusing a previously shutdown one. ## Validation performed - `npm run typecheck` in `services/agent` - `npm run build` in `services/agent` --- services/agent/src/connect-client.ts | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/services/agent/src/connect-client.ts b/services/agent/src/connect-client.ts index 32732ec2..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"); } From 558648250ab43b4d5d3a0ac63440c8d93c8559e9 Mon Sep 17 00:00:00 2001 From: Bazyli Brzoska Date: Sat, 7 Feb 2026 00:49:46 -0800 Subject: [PATCH 14/21] fix(control-plane): prevent stale PVC resume deadlocks ## Root cause A paused session could store a PVC name in Redis that no longer exists. On resume, control-plane reused that stale PVC without validation. This created a sandbox pod that referenced a missing claim and remained Pending. A second race amplified the issue: readiness checks could trust informer cache state from a previous sandbox instance during fast delete/recreate cycles, causing sessions to be marked ready prematurely. ## What changed - Added `PVCExists()` to the Kubernetes runtime interface. - Updated resume sandbox creation to validate stored PVC names before reuse. - If the stored PVC is missing, clear the stale value and create a fresh PVC instead of passing `_EXISTING_PVC_NAME`. - Updated `GetStatus()` to query live API objects by session label first (cache only as fallback on API error). - Updated `WaitForReady()` to use fresh status checks before informer callbacks. - Added regression tests for: - using a valid stored PVC - skipping a missing stored PVC ## Why this fixes the issue Sessions no longer create unschedulable pods from stale PVC references, and readiness logic no longer reports stale cache readiness after sandbox recreation. ## Validation performed - `go test ./...` in `services/control-plane` --- services/control-plane/internal/k8s/client.go | 1 + .../control-plane/internal/k8s/sandbox.go | 84 +++++++++++++------ .../control-plane/internal/session/manager.go | 19 ++++- .../internal/session/manager_test.go | 78 ++++++++++++++++- 4 files changed, 155 insertions(+), 27 deletions(-) diff --git a/services/control-plane/internal/k8s/client.go b/services/control-plane/internal/k8s/client.go index 1c367909..7fd2c655 100644 --- a/services/control-plane/internal/k8s/client.go +++ b/services/control-plane/internal/k8s/client.go @@ -59,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 b8d96351..7f34683b 100644 --- a/services/control-plane/internal/k8s/sandbox.go +++ b/services/control-plane/internal/k8s/sandbox.go @@ -513,13 +513,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 @@ -582,28 +583,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(), @@ -658,6 +682,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{}) diff --git a/services/control-plane/internal/session/manager.go b/services/control-plane/internal/session/manager.go index 2b7c1bee..956b167b 100644 --- a/services/control-plane/internal/session/manager.go +++ b/services/control-plane/internal/session/manager.go @@ -456,8 +456,23 @@ func (m *Manager) createSandboxDirect(ctx context.Context, sessionID string, rep } 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 + 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. Fall back to a fresh PVC + // instead of creating an unschedulable pod that stays stuck in resuming forever. + slog.Warn("Stored PVC not found, creating sandbox with a new PVC", "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) + } + } } } diff --git a/services/control-plane/internal/session/manager_test.go b/services/control-plane/internal/session/manager_test.go index 7a0da415..40fb0b50 100644 --- a/services/control-plane/internal/session/manager_test.go +++ b/services/control-plane/internal/session/manager_test.go @@ -35,6 +35,8 @@ type mockRuntime struct { 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 previewHostname string previewHostErr error } @@ -45,6 +47,8 @@ func newMockRuntime() *mockRuntime { 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), } } @@ -52,6 +56,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 } @@ -235,6 +244,16 @@ 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) { return "agent-home-sess-" + sessionID, nil } @@ -287,6 +306,7 @@ type mockStorage struct { streams map[string][]storage.StreamEntryWithID oauth map[string]*storage.CodexOAuthSessionData oauthGlobal *storage.CodexOAuthSessionData + pvcNames map[string]string } func newMockStorage() *mockStorage { @@ -294,6 +314,7 @@ func newMockStorage() *mockStorage { sessions: make(map[string]*pb.Session), streams: make(map[string][]storage.StreamEntryWithID), oauth: make(map[string]*storage.CodexOAuthSessionData), + pvcNames: make(map[string]string), } } @@ -565,11 +586,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 @@ -597,6 +623,56 @@ 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_SkipsStoredPVCWhenMissing(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.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 TestEnsureActiveSlot_NoActionWhenUnderLimit(t *testing.T) { manager, runtime, _ := newTestManager(3) From c6474a2f524fa80214a3c752b6679d132796d791 Mon Sep 17 00:00:00 2001 From: Bazyli Brzoska Date: Sat, 7 Feb 2026 08:09:10 -0800 Subject: [PATCH 15/21] fix(control-plane): auto-restore latest snapshot when resume PVC is missing ## Root cause When a stored session PVC name was stale, resume previously fell back to creating a fresh empty workspace. This recovered availability but could appear as file loss if no manual snapshot restore was performed. ## What changed - Added automatic snapshot fallback in direct resume flow: - If stored PVC is missing, fetch snapshots and pick the latest one (newest-first). - Attempt PVC restore from that snapshot before sandbox creation. - If snapshot restore fails, log and continue with fresh PVC as last-resort availability fallback. - Extracted snapshot PVC preparation into `preparePVCFromSnapshot()` to reuse restore logic. - Added regression tests for: - missing PVC + no snapshots => no existing PVC injected - missing PVC + snapshots => latest snapshot is restored and injected ## Why this fixes the issue Sessions can now self-heal to the most recent recoverable workspace state when PVC metadata drifts, instead of silently booting into an empty filesystem. ## Validation performed - `go test ./...` in `services/control-plane` --- .../control-plane/internal/session/manager.go | 144 ++++++++++-------- .../internal/session/manager_test.go | 75 ++++++++- 2 files changed, 155 insertions(+), 64 deletions(-) diff --git a/services/control-plane/internal/session/manager.go b/services/control-plane/internal/session/manager.go index 956b167b..1e15b65a 100644 --- a/services/control-plane/internal/session/manager.go +++ b/services/control-plane/internal/session/manager.go @@ -395,65 +395,8 @@ 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) - - // 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) - - // Pass the existing PVC name so sandbox uses it instead of creating a new one - env[k8s.ExistingPVCEnvKey] = pvcName - } else { + // 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) @@ -466,13 +409,45 @@ func (m *Manager) createSandboxDirect(ctx context.Context, sessionID string, rep 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. Fall back to a fresh PVC - // instead of creating an unschedulable pod that stays stuck in resuming forever. - slog.Warn("Stored PVC not found, creating sandbox with a new PVC", "sessionID", sessionID, "pvc", existingPVC) + // Stale Redis state can point to a PVC that no longer exists. + // Prefer restoring from the latest snapshot before falling back to a fresh PVC. + 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) } + + 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.Warn("No snapshots available for missing PVC recovery; creating fresh PVC", "sessionID", sessionID) + } + } + } + } + + // 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 } } @@ -611,6 +586,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 diff --git a/services/control-plane/internal/session/manager_test.go b/services/control-plane/internal/session/manager_test.go index 40fb0b50..6def64b6 100644 --- a/services/control-plane/internal/session/manager_test.go +++ b/services/control-plane/internal/session/manager_test.go @@ -37,6 +37,7 @@ type mockRuntime struct { readyCallbacks map[string][]k8s.SandboxReadyCallback createSandboxEnv map[string]map[string]string pvcExists map[string]bool + restoreSnapshot []string previewHostname string previewHostErr error } @@ -49,6 +50,7 @@ func newMockRuntime() *mockRuntime { readyCallbacks: make(map[string][]k8s.SandboxReadyCallback), createSandboxEnv: make(map[string]map[string]string), pvcExists: make(map[string]bool), + restoreSnapshot: make([]string, 0), } } @@ -255,6 +257,9 @@ func (m *mockRuntime) PVCExists(ctx context.Context, pvcName string) (bool, erro } 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 } @@ -307,6 +312,7 @@ type mockStorage struct { oauth map[string]*storage.CodexOAuthSessionData oauthGlobal *storage.CodexOAuthSessionData pvcNames map[string]string + snapshots map[string][]*pb.Snapshot } func newMockStorage() *mockStorage { @@ -315,6 +321,7 @@ func newMockStorage() *mockStorage { streams: make(map[string][]storage.StreamEntryWithID), oauth: make(map[string]*storage.CodexOAuthSessionData), pvcNames: make(map[string]string), + snapshots: make(map[string][]*pb.Snapshot), } } @@ -542,22 +549,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 } @@ -648,7 +683,7 @@ func TestCreateSandboxDirect_UsesStoredPVCWhenItExists(t *testing.T) { } } -func TestCreateSandboxDirect_SkipsStoredPVCWhenMissing(t *testing.T) { +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()) @@ -673,6 +708,42 @@ func TestCreateSandboxDirect_SkipsStoredPVCWhenMissing(t *testing.T) { } } +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.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) From 9e0637418ee67021aaad8c09f0d28e8f4a949e89 Mon Sep 17 00:00:00 2001 From: Bazyli Brzoska Date: Sat, 7 Feb 2026 08:19:16 -0800 Subject: [PATCH 16/21] fix(control-plane): avoid resume hangs when restore job is absent ## Root cause Session resume from snapshot assumed JuiceFS always creates a Kubernetes Job named `juicefs-restore-snapshot-` in `kube-system`. In this cluster, PVC-from-snapshot can succeed without any restore Job object, so `WaitForRestoreJob` kept polling for a non-existent job and the session stayed in `SESSION_STATUS_RESUMING`. A secondary edge case also existed: when stored Redis `pvcName` was stale but the canonical session PVC (`agent-home-sess-`) still existed, resume still went down snapshot-restore path unnecessarily. ## What changed - Updated missing-PVC recovery in `createSandboxDirect` to: - check for canonical session PVC first - resume with canonical PVC when present - only fall back to latest snapshot when both stored and canonical PVCs are missing - Updated `WaitForRestoreJob` to: - discover restore job in both `kube-system` and control-plane namespace - use bounded discovery timeout (10s) - proceed without blocking when no async restore job appears - still wait for completion/failure when a restore job is observed - treat observed-then-disappeared jobs as completed (TTL cleanup compatibility) - Added/updated manager tests for: - missing stored PVC + no snapshots - missing stored PVC + canonical PVC exists - missing stored PVC + latest snapshot restore ## Why this fixes the issue Resume no longer hard-depends on provider-specific restore-job behavior. If snapshot restore is synchronous or jobless, resume continues instead of hanging. If valid PVC state already exists, resume uses it directly and avoids unnecessary restore paths. ## Validation - `go test ./...` in `services/control-plane` passes. - Logic paths are covered by session manager unit tests for stale PVC scenarios. - Manual validation: - paused an existing session, injected a stale `pvcName` in Redis, then resumed - control-plane logs showed stale-PVC detection and canonical-PVC reuse - session returned to `ready` and accepted a prompt/response roundtrip successfully --- .../control-plane/internal/k8s/sandbox.go | 94 ++++++++++++++++--- .../control-plane/internal/session/manager.go | 33 +++++-- .../internal/session/manager_test.go | 42 ++++++++- 3 files changed, 144 insertions(+), 25 deletions(-) diff --git a/services/control-plane/internal/k8s/sandbox.go b/services/control-plane/internal/k8s/sandbox.go index 7f34683b..f8953c3e 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" @@ -1977,9 +1978,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) @@ -2014,23 +2016,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 } @@ -2038,10 +2100,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/manager.go b/services/control-plane/internal/session/manager.go index 1e15b65a..be651eca 100644 --- a/services/control-plane/internal/session/manager.go +++ b/services/control-plane/internal/session/manager.go @@ -410,20 +410,37 @@ func (m *Manager) createSandboxDirect(ctx context.Context, sessionID string, rep env[k8s.ExistingPVCEnvKey] = existingPVC default: // Stale Redis state can point to a PVC that no longer exists. - // Prefer restoring from the latest snapshot before falling back to a fresh PVC. + // 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) } - 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) + 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) + } + + 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 { - slog.Warn("No snapshots available for missing PVC recovery; creating fresh PVC", "sessionID", sessionID) + // 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.Warn("No snapshots available for missing PVC recovery; creating fresh PVC", "sessionID", sessionID) + } } } } diff --git a/services/control-plane/internal/session/manager_test.go b/services/control-plane/internal/session/manager_test.go index 6def64b6..af7f9295 100644 --- a/services/control-plane/internal/session/manager_test.go +++ b/services/control-plane/internal/session/manager_test.go @@ -317,10 +317,10 @@ type mockStorage struct { func newMockStorage() *mockStorage { return &mockStorage{ - sessions: make(map[string]*pb.Session), - streams: make(map[string][]storage.StreamEntryWithID), - oauth: make(map[string]*storage.CodexOAuthSessionData), - pvcNames: make(map[string]string), + 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), } } @@ -695,6 +695,7 @@ func TestCreateSandboxDirect_SkipsStoredPVCWhenMissingAndNoSnapshots(t *testing. 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) @@ -708,6 +709,38 @@ func TestCreateSandboxDirect_SkipsStoredPVCWhenMissingAndNoSnapshots(t *testing. } } +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" @@ -725,6 +758,7 @@ func TestCreateSandboxDirect_RestoresLatestSnapshotWhenStoredPVCMissing(t *testi 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) From f8fbd3888baef016e4d5eea0f13a1a52118075f3 Mon Sep 17 00:00:00 2001 From: Bazyli Brzoska Date: Sat, 7 Feb 2026 13:02:41 -0800 Subject: [PATCH 17/21] feat(devflow): add fast remote build/deploy iteration workflow ## Root cause Developer iteration required a slow sequence of local builds, registry push/pull, and broad deployment steps. There was no dedicated fast-path for backend/agent-only changes, and remote build prerequisites were not codified. ## What changed - Added script-based dev loop tooling: - `scripts/dev/build-on-remote.sh` - `scripts/dev/deploy-dev-images.sh` - `scripts/dev/verify-dev-loop.sh` - Added Ansible-based dev flow: - `infra/ansible/playbooks/dev-loop.yaml` (dev-build/dev-deploy/dev-verify phases) - `infra/ansible/playbooks/dev-builder.yaml` - `infra/ansible/roles/dev-builder` (installs Docker + Buildx and configures builder user) - Added Make targets for both paths: - `dev-install-builder` - `dev-loop-ansible`, `dev-loop-ansible-build`, `dev-loop-ansible-deploy`, `dev-loop-ansible-verify` - `dev-loop-remote` and granular script targets - Improved dev deployment behavior: - force `imagePullPolicy=IfNotPresent` for control-plane and sandbox template in dev flow - keep control-plane `AGENT_IMAGE` and sandbox template image synchronized - refresh warm pool after agent image updates - Improved Docker rebuild speed: - optimized `services/control-plane/Dockerfile` layer ordering and module download caching - optimized `services/agent/Dockerfile` dependency-layer caching (manifests copied before source) - Tightened build/sync footprint: - reduced remote sync to Docker-required service paths - updated `.dockerignore` to exclude non-required top-level paths for root-context agent builds - Added and updated documentation: - `docs/dev-iteration.md` - `docs/deployment.md` - `infra/ansible/README.md` - `README.md` ## Why this fixes the issue The new workflow supports fast backend/agent iteration by building on the target host and deploying only runtime deltas. It removes mandatory registry round-trips from the inner loop and avoids full-manifest convergence for code-only updates. Ansible and script paths now both support quick build/deploy/verify cycles with explicit prerequisites and clearer failure modes. ## Validation performed - Verified script syntax: - `bash -n scripts/dev/build-on-remote.sh scripts/dev/deploy-dev-images.sh scripts/dev/verify-dev-loop.sh` - Verified playbook syntax: - `ansible-playbook playbooks/dev-builder.yaml --syntax-check` - `ansible-playbook playbooks/dev-loop.yaml --syntax-check` - Executed on real target host: - `make dev-install-builder ANSIBLE_USER=ubuntu` - `make dev-loop-remote TAG=dev-full-script-...` (full pass) - `make dev-loop-ansible ANSIBLE_USER=ubuntu TAG=dev-full-ansible-...` (full pass) - `make dev-build-remote-control-plane ...` (pass) - `make dev-build-remote-agent ...` (pass) - `make dev-loop-ansible-build ...` (pass) - `make dev-loop-ansible-deploy ...` (pass) - `make dev-loop-ansible-verify ...` (pass) --- .dockerignore | 7 +- Makefile | 69 ++- README.md | 1 + docs/deployment.md | 31 ++ docs/dev-iteration.md | 139 ++++++ infra/ansible/README.md | 19 + infra/ansible/playbooks/dev-builder.yaml | 14 + infra/ansible/playbooks/dev-loop.yaml | 419 ++++++++++++++++++ .../roles/dev-builder/defaults/main.yaml | 6 + .../ansible/roles/dev-builder/tasks/main.yaml | 28 ++ scripts/dev/build-on-remote.sh | 107 +++++ scripts/dev/deploy-dev-images.sh | 54 +++ scripts/dev/verify-dev-loop.sh | 27 ++ services/agent/Dockerfile | 10 +- services/control-plane/Dockerfile | 6 +- 15 files changed, 931 insertions(+), 6 deletions(-) create mode 100644 docs/dev-iteration.md create mode 100644 infra/ansible/playbooks/dev-builder.yaml create mode 100644 infra/ansible/playbooks/dev-loop.yaml create mode 100644 infra/ansible/roles/dev-builder/defaults/main.yaml create mode 100644 infra/ansible/roles/dev-builder/tasks/main.yaml create mode 100755 scripts/dev/build-on-remote.sh create mode 100755 scripts/dev/deploy-dev-images.sh create mode 100755 scripts/dev/verify-dev-loop.sh 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/Makefile b/Makefile index c851aeda..8cbe897c 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,10 @@ 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 ?= TEAM_ID_AUTO ?= $(shell \ team=$$(security find-certificate -a -c "Apple Development" -p "$$HOME/Library/Keychains/login.keychain-db" 2>/dev/null | \ @@ -20,7 +25,7 @@ ifneq ($(strip $(TEAM_ID)),) XCODE_SIGN_ARGS += DEVELOPMENT_TEAM=$(TEAM_ID) endif -.PHONY: rollout rollout-control-plane rollout-agent deploy test-ios run-macos run-ios run-device print-ios-team-id proto proto-lint proto-breaking proto-setup +.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 @@ -69,6 +74,68 @@ 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 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/docs/deployment.md b/docs/deployment.md index 5d7cda75..4551e2eb 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -253,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/infra/ansible/README.md b/infra/ansible/README.md index 8bdb3c15..59b9cf0b 100644 --- a/infra/ansible/README.md +++ b/infra/ansible/README.md @@ -180,6 +180,24 @@ ansible-playbook playbooks/k8s-only.yaml \ -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" ``` @@ -246,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) 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/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/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 8ab224c7..4beecd4f 100644 --- a/services/agent/Dockerfile +++ b/services/agent/Dockerfile @@ -53,14 +53,18 @@ 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 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 \ From 8702ec81ffd24620c0434ab55336ec492bba5593 Mon Sep 17 00:00:00 2001 From: Bazyli Brzoska Date: Sun, 8 Feb 2026 22:34:37 -0800 Subject: [PATCH 18/21] chore: update agent versions to latest versions --- services/agent/Dockerfile | 7 ++++--- services/agent/package-lock.json | 8 ++++---- services/agent/package.json | 2 +- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/services/agent/Dockerfile b/services/agent/Dockerfile index 4beecd4f..c1c3b2dc 100644 --- a/services/agent/Dockerfile +++ b/services/agent/Dockerfile @@ -163,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 && \ @@ -172,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/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" }, From b49bb32e4e9704141e659b22b90a895442576f2c Mon Sep 17 00:00:00 2001 From: Bazyli Brzoska Date: Sun, 8 Feb 2026 22:43:03 -0800 Subject: [PATCH 19/21] feat(ios): improve model defaults and picker selection UX ## Root cause Model defaults were hardcoded per SDK and could drift from backend model availability. Claude Opus selection also mis-ranked date-stamped IDs (for example `claude-opus-4-20250514`) above semantic versions like `4.6`, and the expanded model picker did not reveal the selected row by default. ## What changed - Added dynamic preferred-model resolution in `UnifiedModelsStore`: - prefer last-used model when still available - otherwise select latest detected model family from backend-provided list - keep static default as final fallback - Implemented version parsing/ranking helpers for Codex and Claude Opus model families. - Fixed Claude Opus ranking to prioritize semantic version extraction and ignore date-like suffixes when determining highest version. - Added per-SDK last-used model persistence in `SettingsStore` via `UserDefaults`. - Updated `PromptSheet` to initialize/reconcile selected model IDs from persisted/backend-derived preferences and persist on submit. - Updated inline model picker dropdown to auto-scroll to the currently selected model when expanded. - Updated root `AGENTS.md` with a rule to read `clients/*/README.md` before changing client code. ## Why this fixes the issue Selection now follows backend model data instead of stale hardcoded IDs, while still honoring user preference when available. Claude Opus version ordering now compares semantic versions correctly, so newer releases (for example `4.6`) are selected over older major-only variants. Auto-scrolling reduces picker friction by making current context visible immediately. ## Validation performed - Built macOS client repeatedly using repo workflow command: - `make run-macos` - Verified successful build after each functional change. --- AGENTS.md | 1 + .../Features/Sessions/ModelPickerSheet.swift | 45 +++- .../Features/Sessions/PromptSheet.swift | 103 ++++++++- .../ios/Netclode/Stores/SettingsStore.swift | 16 ++ .../Netclode/Stores/UnifiedModelsStore.swift | 210 ++++++++++++++++++ 5 files changed, 360 insertions(+), 15 deletions(-) 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/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 15aa0757..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,6 +560,82 @@ 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 } @@ -580,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/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) + } } From 4cfedad542ee60d4663a2339ee7b6cd41e8433c5 Mon Sep 17 00:00:00 2001 From: Bazyli Brzoska Date: Sun, 8 Feb 2026 22:46:48 -0800 Subject: [PATCH 20/21] chore: update go.work.sum; follow up to f0306a2 --- go.work.sum | 9 +++++++++ 1 file changed, 9 insertions(+) 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= From ab22b0d638fdefb6f5302a2fbb57e4557681524d Mon Sep 17 00:00:00 2001 From: Bazyli Brzoska Date: Sun, 8 Feb 2026 22:56:51 -0800 Subject: [PATCH 21/21] fix(control-plane): make port unexpose idempotent on missing service ## Root cause `Manager.UnexposePort` returned early on any runtime error before emitting `port_unexposed`. When a sandbox had already been cleaned up, `k8sRuntime.UnexposePort` could fail on `Services().Get(...)` with NotFound, so the unexpose event was never persisted. On resume, `restoreExposedPorts` reconstructed state from old `port_exposed` events and re-exposed the port. ## What changed - Updated `k8sRuntime.UnexposePort` to treat missing Tailscale Service as a no-op and continue. - Updated `Manager.UnexposePort` to ignore Kubernetes NotFound errors and still emit/persist `port_unexposed`. - Added regression coverage for the NotFound path to ensure unexpose remains durable. ## Why this fixes the issue Unexpose now behaves idempotently when infrastructure was already removed, and state persistence is no longer coupled to best-effort cleanup of Kubernetes objects. The `port_unexposed` event is always written, so restore logic no longer re-exposes ports that were intentionally unexposed. ## Validation performed - `go test ./services/control-plane/internal/session` - `go test ./services/control-plane/internal/k8s` --- .../control-plane/internal/k8s/sandbox.go | 36 ++++++++++-------- .../control-plane/internal/session/manager.go | 6 ++- .../internal/session/manager_test.go | 38 +++++++++++++++++++ 3 files changed, 63 insertions(+), 17 deletions(-) diff --git a/services/control-plane/internal/k8s/sandbox.go b/services/control-plane/internal/k8s/sandbox.go index f8953c3e..f31a6767 100644 --- a/services/control-plane/internal/k8s/sandbox.go +++ b/services/control-plane/internal/k8s/sandbox.go @@ -1017,29 +1017,33 @@ func (r *k8sRuntime) ExposePort(ctx context.Context, sessionID string, port int) 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 { - return fmt.Errorf("get tailscale service: %w", err) - } - - removedServicePort := false - servicePorts := make([]corev1.ServicePort, 0, len(svc.Spec.Ports)) - for _, p := range svc.Spec.Ports { - if p.Port == int32(port) { - removedServicePort = true - continue + 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) } - 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) + 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) } - slog.Info("Removed port from Tailscale service", "sessionID", sessionID, "port", port) } // 2. Remove port from the NetworkPolicy diff --git a/services/control-plane/internal/session/manager.go b/services/control-plane/internal/session/manager.go index be651eca..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 ( @@ -1176,7 +1177,10 @@ func (m *Manager) ExposePort(ctx context.Context, sessionID string, port int) (s // 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 { - return err + if !k8serrors.IsNotFound(err) { + return err + } + slog.Info("Unexpose target not found, persisting event anyway", "sessionID", sessionID, "port", port, "error", err) } port32 := int32(port) diff --git a/services/control-plane/internal/session/manager_test.go b/services/control-plane/internal/session/manager_test.go index af7f9295..606c8c79 100644 --- a/services/control-plane/internal/session/manager_test.go +++ b/services/control-plane/internal/session/manager_test.go @@ -19,6 +19,8 @@ import ( "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 @@ -40,6 +42,7 @@ type mockRuntime struct { restoreSnapshot []string previewHostname string previewHostErr error + unexposePortErr error } func newMockRuntime() *mockRuntime { @@ -166,6 +169,9 @@ func (m *mockRuntime) ExposePort(ctx context.Context, sessionID string, port int 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) } @@ -1464,6 +1470,38 @@ func TestUnexposePort_RemovesPortAndPersistsEvent(t *testing.T) { } } +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)