9 Commits
v1.0.1 ... main

Author SHA1 Message Date
Louie
7455f76351 feat: match Go TLS fingerprint for MITM upstream (#11)
* feat: match Go TLS fingerprint for MITM upstream connections

Replace rustls with boring2 (BoringSSL) for all MITM→Google upstream
connections, configured with Go crypto/tls exact defaults:

- Cipher suites: TLS_AES_128_GCM_SHA256 + 14 others in Go order
- Curves: X25519, P-256, P-384
- Signature algorithms: ECDSA+SHA256, RSA-PSS+SHA256, etc.
- HTTP/2 SETTINGS: 4MB stream window, 1GB connection window, 10MB
  header list, no adaptive windowing

Local TLS (LS→MITM) still uses rustls for CA cert presentation.
boring2/tokio-boring2 were already compiled as transitive deps from
wreq — no new build time added.

* chore: fmt + update README TLS description
2026-02-18 16:15:08 -06:00
Nikketryhard
e1ea22c7f0 feat: add discussion link about open source status to README 2026-02-18 14:45:41 -06:00
Nikketryhard
45b5cc15e0 refactor: remove automatic service/task installation from Linux, macOS, and Windows setup scripts. 2026-02-18 13:54:43 -06:00
Nikketryhard
9f36cc81d7 feat: Add GitHub issue templates for bug reports and feature requests. 2026-02-18 13:53:32 -06:00
Louie
6fd7cf6618 fix: add Antigravity prereq check to all setup scripts (#7)
All three setup scripts (Linux, macOS, Windows) now verify that the
Antigravity app is installed and the LS binary exists before proceeding.
Fails early with a clear error message and suggestions instead of a
cryptic runtime crash.

Closes #5
2026-02-18 13:17:31 -06:00
Louie
4966d8f648 Merge pull request #6 from Kazuki-0147/fix/headless-timeout
fix: avoid HTTPS_PROXY conflict with DNS redirect in headless mode
2026-02-18 13:08:43 -06:00
Kazuki-0147
60d7cd677e fix: avoid HTTPS_PROXY when DNS redirect is active in headless mode
When LD_PRELOAD DNS redirect is active, setting HTTPS_PROXY causes Go's
net/http to send HTTP CONNECT requests through the MITM proxy. However,
the MITM proxy expects direct TLS connections for SNI-based interception,
not CONNECT tunneling. This mismatch causes all non-gRPC calls (OAuth
token refresh, fetchUserInfo, etc.) to fail with EOF/timeout errors.

Changes:
- Only set HTTPS_PROXY/HTTP_PROXY as fallback when DNS redirect SO is
  not available
- Add GODEBUG=netdns=cgo to force Go's cgo (libc) DNS resolver, since
  the pure-Go resolver bypasses LD_PRELOAD getaddrinfo() hooks entirely

Fixes #4
2026-02-18 19:00:53 +00:00
Nikketryhard
03f44bc126 docs: document additional LS services, memory system, cascade configurations, supercomplete features, and browser automation policies. 2026-02-18 12:59:48 -06:00
Nikketryhard
29bebd79ea docs: add OS compatibility note to early stage warning 2026-02-18 04:16:02 -06:00
15 changed files with 965 additions and 170 deletions

102
.github/ISSUE_TEMPLATE/bug_report.yml vendored Normal file
View File

@@ -0,0 +1,102 @@
name: Bug Report
description: Something isn't working right
labels: ["bug"]
body:
- type: markdown
attributes:
value: |
Thanks for reporting! Please fill out the info below so we can debug faster.
- type: input
id: os
attributes:
label: Operating System
description: e.g. Ubuntu 24.04, macOS 15.2, Windows 11
placeholder: Ubuntu 24.04
validations:
required: true
- type: dropdown
id: mode
attributes:
label: Running Mode
description: How are you running ZeroGravity?
options:
- Headless (default, --headless)
- Classic (--classic, attached to running Antigravity)
- No MITM (--no-mitm)
validations:
required: true
- type: input
id: version
attributes:
label: ZeroGravity Version / Commit
description: "Output of `git rev-parse --short HEAD` or the version you downloaded"
placeholder: 4966d8f
validations:
required: true
- type: input
id: model
attributes:
label: Model Used
description: Which model were you using when the issue occurred?
placeholder: gemini-3-flash
- type: textarea
id: description
attributes:
label: What happened?
description: Describe the issue clearly. What did you expect vs what actually happened?
validations:
required: true
- type: textarea
id: logs
attributes:
label: Logs
description: |
Paste relevant log output. You can get logs with:
- `zg logs-all` (full logs)
- `journalctl --user -u zerogravity --no-pager -n 200` (systemd)
- Or check `~/.config/zerogravity/` for log files
render: text
- type: textarea
id: trace
attributes:
label: Call Trace
description: |
If available, paste the trace summary for the failing request.
Traces are saved to `~/.config/zerogravity/traces/YYYY-MM-DD/`.
You can get the latest one with:
```
TRACE_DIR=~/.config/zerogravity/traces/$(date +%Y-%m-%d)
cat "$TRACE_DIR/$(ls -t "$TRACE_DIR" | head -1)/summary.md"
```
render: markdown
- type: textarea
id: health
attributes:
label: Health Check Output
description: |
Paste the output of:
```
curl -s http://localhost:8741/health | jq .
zg status
```
render: json
- type: textarea
id: repro
attributes:
label: Steps to Reproduce
description: How can we reproduce this? Include the curl command or client config if applicable.
- type: textarea
id: extra
attributes:
label: Additional Context
description: Anything else — screenshots, network setup (VPN/proxy), Antigravity version, etc.

View File

@@ -0,0 +1,23 @@
name: Feature Request
description: Suggest an idea or improvement
labels: ["enhancement"]
body:
- type: textarea
id: description
attributes:
label: What do you want?
description: Describe the feature or improvement you'd like to see.
validations:
required: true
- type: textarea
id: usecase
attributes:
label: Use Case
description: Why do you need this? What problem does it solve?
- type: textarea
id: alternatives
attributes:
label: Alternatives Considered
description: Any workarounds or alternative approaches you've tried?

2
Cargo.lock generated
View File

@@ -2366,6 +2366,7 @@ dependencies = [
"async-stream", "async-stream",
"axum", "axum",
"base64", "base64",
"boring2",
"brotli 7.0.0", "brotli 7.0.0",
"bytes", "bytes",
"chrono", "chrono",
@@ -2386,6 +2387,7 @@ dependencies = [
"serde_json", "serde_json",
"time", "time",
"tokio", "tokio",
"tokio-boring2",
"tokio-rustls", "tokio-rustls",
"tokio-stream", "tokio-stream",
"tower-http", "tower-http",

View File

@@ -40,6 +40,8 @@ rustls = { version = "0.23", features = ["ring"] }
tokio-rustls = "0.26" tokio-rustls = "0.26"
rustls-native-certs = "0.8" rustls-native-certs = "0.8"
rustls-pemfile = "2" rustls-pemfile = "2"
boring2 = "5.0.0-alpha.12"
tokio-boring2 = "5.0.0-alpha.12"
time = "0.3" time = "0.3"
base64 = "0.22" base64 = "0.22"
httparse = "1" httparse = "1"

View File

@@ -20,8 +20,12 @@
> **Early stage.** Ran this on OpenCode with an Ultra account for 3 days straight, stress testing the whole time. No issues so far. For comparison, a Pro account got banned after about 3 hours using CLIProxyAPI / Antigravity Manager. Not using a firewall since I'm living overseas, so your situation may vary if google detects any suspicious activity. PS: The tool call part is still unstable and may hang sometimes. > **Early stage.** Ran this on OpenCode with an Ultra account for 3 days straight, stress testing the whole time. No issues so far. For comparison, a Pro account got banned after about 3 hours using CLIProxyAPI / Antigravity Manager. Not using a firewall since I'm living overseas, so your situation may vary if google detects any suspicious activity. PS: The tool call part is still unstable and may hang sometimes.
> >
> This software is developed on Linux. I aim to support every OS as best as possible, so if there is any issue please open an issue and I will be happy to assist.
>
> Star the repo so more people can find it while it still works. Issues and PRs are welcome. > Star the repo so more people can find it while it still works. Issues and PRs are welcome.
> **Should this stay open source?** [I need your help. Read the discussion and share your thoughts.](https://github.com/NikkeTryHard/zerogravity/discussions/10)
--- ---
## Skip Reading This — It's the Age of AI ## Skip Reading This — It's the Age of AI
@@ -57,7 +61,7 @@ Explain to the user what this project unlocks — not what it _is_, but what bec
## How It Works ## How It Works
The LS or Language Server is Antigravity's closed source Go binary that talks to Google's API over gRPC. The Extension Server is what feeds it auth tokens and settings/configs, we fake it with a stub so the LS thinks it's inside a real Antigravity window. ZeroGravity turns your OpenAI-compatible requests into dummy prompts and tells the LS to make an API call. The MITM proxy intercepts that call before it leaves the machine, swaps in your real prompt, tools, images, and generation params, re-encrypts it with BoringSSL matching Chrome's exact TLS fingerprint, and forwards it to Google. Google sees what looks like a normal Antigravity session. The response streams back as SSE events which the MITM parses for text, thinking tokens, tool calls, and usage. The iptables redirect is a UID-scoped firewall rule that routes only the LS's traffic through the MITM without touching anything else. The LS or Language Server is Antigravity's closed source Go binary that talks to Google's API over gRPC. The Extension Server is what feeds it auth tokens and settings/configs, we fake it with a stub so the LS thinks it's inside a real Antigravity window. ZeroGravity turns your OpenAI-compatible requests into dummy prompts and tells the LS to make an API call. The MITM proxy intercepts that call before it leaves the machine, swaps in your real prompt, tools, images, and generation params, re-encrypts it over TLS, and forwards it to Google. All proxy-to-LS communication uses BoringSSL with Chrome's exact TLS and HTTP/2 fingerprint so the LS can't tell it's not a real Antigravity window. Google sees what looks like a normal Antigravity session. The response streams back as SSE events which the MITM parses for text, thinking tokens, tool calls, and usage. The iptables redirect is a UID-scoped firewall rule that routes only the LS's traffic through the MITM without touching anything else.
```mermaid ```mermaid
%%{init: {'theme': 'dark', 'themeVariables': {'primaryColor': '#2a2a2a', 'primaryTextColor': '#d0d0d0', 'primaryBorderColor': '#888', 'lineColor': '#888', 'secondaryColor': '#333', 'tertiaryColor': '#3a3a3a', 'edgeLabelBackground': '#2a2a2a', 'nodeTextColor': '#d0d0d0'}}}%% %%{init: {'theme': 'dark', 'themeVariables': {'primaryColor': '#2a2a2a', 'primaryTextColor': '#d0d0d0', 'primaryBorderColor': '#888', 'lineColor': '#888', 'secondaryColor': '#333', 'tertiaryColor': '#3a3a3a', 'edgeLabelBackground': '#2a2a2a', 'nodeTextColor': '#d0d0d0'}}}%%

View File

@@ -505,6 +505,76 @@ Team/organization management. Handles licensing, seats, credits.
- `UpdateCascadeWebSearchEnabled` — toggle web search - `UpdateCascadeWebSearchEnabled` — toggle web search
- `SetUserApiProviderKey` — BYOK key management - `SetUserApiProviderKey` — BYOK key management
### AnalyticsService (7+ methods)
Separate analytics service for recording telemetry and training data.
```
BatchRecordCompletions
BatchRecordPrompts
RecordCommandUsage
RecordCompletions
RecordContextToPrompt
RecordCortexTrajectory
RecordCortexTrajectoryStep
```
**Notable:** Distinct from `ApiServerService.Record*` — likely a dedicated analytics pipeline.
### UserAnalyticsService (5+ methods)
Per-user analytics dashboard service.
```
Analytics
CascadeAnalytics
GetAnalytics
GetPreferredTimeZone
UserPageAnalytics
```
### KnowledgeBaseService (17+ methods)
Enterprise knowledge base with multi-source ingestion. Uses OpenSearch backend.
```
AddGithubUsers
AddUsers
CancelKnowledgeBaseJobs
ConnectKnowledgeBaseAccount
DeleteKnowledgeBaseConnection
ForwardSlackPayload
GetConnectorInternalConfig
GetKnowledgeBaseConnectorState
GetKnowledgeBaseItemsFromScopeItems
GetKnowledgeBaseJobStates
GetKnowledgeBaseScopeItems
GetKnowledgeBaseWebhookUrl
IngestGithubData
IngestGoogleDriveData
IngestJiraData
IngestJiraPayload
IngestSlackData
IngestSlackPayload
KnowledgeBaseSearch
UpdateConnectorConfig
```
**Key insight:** Supports ingesting from **GitHub**, **Google Drive**, **Jira**, and **Slack** — full enterprise knowledge integration.
### CodeIndexService (4+ methods)
Code search and indexing. Uses OpenSearch for hybrid vector+keyword search.
```
GraphSearch
HybridSearch
OpenSearchAddRepository
OpenSearchGetIndex
```
**Notable:** `GraphSearch` suggests code graph traversal, `HybridSearch` combines vector embeddings with keyword matching.
### ModelManagementService (8 methods) ### ModelManagementService (8 methods)
Self-hosted model management for hybrid deployments. Self-hosted model management for hybrid deployments.
@@ -935,6 +1005,539 @@ https://iamcredentials.googleapis.com/v1/ ← Service account impersonation
--- ---
## Memory & Brain System
The LS has a sophisticated memory system that persists across conversations.
### Memory Sources
```
MEMORY_SOURCE_UNSPECIFIED
MEMORY_SOURCE_USER — manually created by user
MEMORY_SOURCE_CASCADE — extracted from cascade conversations
MEMORY_SOURCE_AUTO_CASCADE — automatically generated from cascades
```
### Memory Triggers
```
MEMORY_TRIGGER_UNSPECIFIED
MEMORY_TRIGGER_ALWAYS_ON — always active
MEMORY_TRIGGER_GLOB — glob pattern match on file paths
MEMORY_TRIGGER_MANUAL — only when explicitly invoked
MEMORY_TRIGGER_MODEL_DECISION — model decides when to use
```
### Memory Actions
```
MEMORY_ACTION_TYPE_UNSPECIFIED
MEMORY_ACTION_TYPE_CREATE
MEMORY_ACTION_TYPE_UPDATE
MEMORY_ACTION_TYPE_DELETE
```
### Brain Entry Types
```
BRAIN_ENTRY_TYPE_UNSPECIFIED
BRAIN_ENTRY_TYPE_PLAN — implementation plans
BRAIN_ENTRY_TYPE_TASK — task checklists
```
### Brain Update Triggers
```
BRAIN_UPDATE_TRIGGER_UNSPECIFIED
BRAIN_UPDATE_TRIGGER_USER_REQUESTED — user explicitly asks
BRAIN_UPDATE_TRIGGER_USER_NEW_INFO — user provides new info
BRAIN_UPDATE_TRIGGER_RESEARCH_NEW_INFO — research discovers new info
BRAIN_UPDATE_TRIGGER_SYSTEM_FORCED — system-initiated update
```
### Brain Filter Strategies
```
BRAIN_FILTER_STRATEGY_UNSPECIFIED
BRAIN_FILTER_STRATEGY_NO_MEMORIES — exclude all memories
BRAIN_FILTER_STRATEGY_NO_SYSTEM_INJECTED_STEPS — exclude system steps
```
---
## Expanded Cascade Configuration Keys
Full list of all cascade config keys extracted from binary (60+ keys):
### Core Behavior
```
CASCADE_ENFORCE_QUOTA — quota enforcement toggle
CASCADE_BASE_MODEL_ID — default model selection
CASCADE_DEFAULT_MODEL_OVERRIDE — model override
CASCADE_GLOBAL_CONFIG_OVERRIDE — global cascade config
CASCADE_MEMORY_CONFIG_OVERRIDE — memory subsystem config
CASCADE_PLAN_BASED_CONFIG_OVERRIDE — planner config
CASCADE_FREE_CONFIG_OVERRIDE — free-tier config
```
### Tool Access
```
CASCADE_ENABLE_MCP_TOOLS — MCP tool use
CASCADE_ANTIGRAVITY_BROWSER_TOOLS_ENABLED
CASCADE_ENABLE_PROXY_WEB_SERVER — proxy web server
CASCADE_ENABLE_IDE_TERMINAL_EXECUTION
CASCADE_VIEW_FILE_TOOL_CONFIG_OVERRIDE
CASCADE_USE_REPLACE_CONTENT_EDIT_TOOL
```
### Web Search
```
CASCADE_WEB_SEARCH_TOOL_ENABLED
CASCADE_WEB_SEARCH_TOOL_DISABLED
CASCADE_WEB_SEARCH_TOOL_UNSPECIFIED
CASCADE_WEB_APP_DEPLOYMENTS_ENABLED
```
### Extension Code Execution
```
CASCADE_RUN_EXTENSION_CODE_ENABLED
CASCADE_RUN_EXTENSION_CODE_DISABLED
CASCADE_RUN_EXTENSION_CODE_ONLY
CASCADE_RUN_EXTENSION_CODE_AUTO_RUN_ENABLED
CASCADE_RUN_EXTENSION_CODE_AUTO_RUN_MODEL_DECIDES
CASCADE_RUN_EXTENSION_CODE_AUTO_RUN_UNSPECIFIED
```
### Command Auto-Execution Policy
```
CASCADE_COMMANDS_AUTO_EXECUTION_UNSPECIFIED
CASCADE_COMMANDS_AUTO_EXECUTION_OFF — always ask
CASCADE_COMMANDS_AUTO_EXECUTION_AUTO — auto-run safe commands
CASCADE_COMMANDS_AUTO_EXECUTION_EAGER — auto-run most commands
```
### Memory & Learning
```
CASCADE_ENABLE_AUTOMATED_MEMORIES — auto-generate memories
CASCADE_USER_MEMORIES_IN_SYS_PROMPT
CASCADE_ENABLE_CUSTOM_RECIPES
CASCADE_BACKGROUND_RESEARCH_CONFIG_OVERRIDE
CASCADE_USE_EXPERIMENT_CHECKPOINTER
CASCADE_USE_SUBAGENT_CHECKPOINTER
```
### Autocomplete & Input
```
CASCADE_INPUT_AUTOCOMPLETE_ENABLED
CASCADE_INPUT_AUTOCOMPLETE_DISABLED
CASCADE_INPUT_AUTOCOMPLETE_UNSPECIFIED
CASCADE_RECIPES_AT_MENTION_VISIBILITY
```
### Run Status
```
CASCADE_RUN_STATUS_UNSPECIFIED
CASCADE_RUN_STATUS_IDLE
CASCADE_RUN_STATUS_BUSY
CASCADE_RUN_STATUS_RUNNING
CASCADE_RUN_STATUS_CANCELING
```
### Edit Importance (for code diffs)
```
CASCADE_EDIT_IMPORTANCE_UNSPECIFIED
CASCADE_EDIT_IMPORTANCE_LOW
CASCADE_EDIT_IMPORTANCE_MEDIUM
CASCADE_EDIT_IMPORTANCE_HIGH
```
### Seat Types
```
CASCADE_SEAT_TYPE_UNSPECIFIED
CASCADE_SEAT_TYPE_ENTRY — entry tier
CASCADE_SEAT_TYPE_STANDARD — standard tier
```
---
## Supercomplete (Tab Completion) System
The "Supercomplete" system powers multi-line tab completions, distinct from basic autocomplete.
### Trigger Conditions
```
SUPERCOMPLETE_TRIGGER_CONDITION_UNSPECIFIED
SUPERCOMPLETE_TRIGGER_CONDITION_TYPING
SUPERCOMPLETE_TRIGGER_CONDITION_AUTOCOMPLETE_ACCEPT
SUPERCOMPLETE_TRIGGER_CONDITION_AUTOCOMPLETE_PREDICTIVE
SUPERCOMPLETE_TRIGGER_CONDITION_SUPERCOMPLETE_ACCEPT
SUPERCOMPLETE_TRIGGER_CONDITION_SUPERCOMPLETE_PREDICTIVE
SUPERCOMPLETE_TRIGGER_CONDITION_TAB_JUMP_ACCEPT
SUPERCOMPLETE_TRIGGER_CONDITION_TAB_JUMP_EDIT
SUPERCOMPLETE_TRIGGER_CONDITION_TAB_JUMP_PREDICTIVE
SUPERCOMPLETE_TRIGGER_CONDITION_CURSOR_LINE_NAVIGATION
SUPERCOMPLETE_TRIGGER_CONDITION_FORCED
```
### Filters
```
SUPERCOMPLETE_FILTER_PREFIX_MATCH
SUPERCOMPLETE_FILTER_SUFFIX_MATCH
SUPERCOMPLETE_FILTER_INSERTION_CAP
SUPERCOMPLETE_FILTER_DELETION_CAP
SUPERCOMPLETE_FILTER_NO_OP
SUPERCOMPLETE_FILTER_REVERT
SUPERCOMPLETE_FILTER_PREVIOUSLY_SHOWN
SUPERCOMPLETE_FILTER_WHITESPACE_ONLY
SUPERCOMPLETE_FILTER_SCORE_THRESHOLD
```
### Config Keys
```
SUPERCOMPLETE_TEMPERATURE
SUPERCOMPLETE_MODEL_CONFIG
SUPERCOMPLETE_LINE_RADIUS
SUPERCOMPLETE_MIN_SCORE
SUPERCOMPLETE_MAX_INSERTIONS
SUPERCOMPLETE_MAX_DELETIONS
SUPERCOMPLETE_MAX_TRAJECTORY_STEPS
SUPERCOMPLETE_MAX_TRAJECTORY_STEP_SIZE
SUPERCOMPLETE_MAX_CONCURRENT_REQUESTS
SUPERCOMPLETE_FAST_DEBOUNCE
SUPERCOMPLETE_REGULAR_DEBOUNCE
SUPERCOMPLETE_RECENT_STEPS_DURATION
SUPERCOMPLETE_USE_CODE_DIAGNOSTICS
SUPERCOMPLETE_CODE_DIAGNOSTICS_TOP_K
SUPERCOMPLETE_DIAGNOSTIC_SEVERITY_THRESHOLD
SUPERCOMPLETE_DISABLE_TYPING_CACHE
SUPERCOMPLETE_DONT_FILTER_MID_STREAMED
SUPERCOMPLETE_ALWAYS_USE_CACHE_ON_EQUAL_STATE
SUPERCOMPLETE_CACHE_ON_PARENT_ID_KILL_SWITCH
SUPERCOMPLETE_ON_ACCEPT_ONLY
SUPERCOMPLETE_PRUNE_RESPONSE
SUPERCOMPLETE_PRUNE_MAX_INSERT_DELETE_LINE_DELTA
SUPERCOMPLETE_INLINE_PURE_DELETE
SUPERCOMPLETE_INLINE_RICH_GHOST_TEXT_INSERTIONS
SUPERCOMPLETE_USE_CURRENT_LINE
SUPERCOMPLETE_NO_ACTIVE_NODE
```
### Tab Jump (Cursor Navigation)
```
TAB_JUMP_ENABLED
TAB_JUMP_LINE_RADIUS
TAB_JUMP_CUMULATIVE_PROMPT_CONFIG
TAB_JUMP_FILTER_IN_SELECTION
TAB_JUMP_FILTER_INSERTION_CAP
TAB_JUMP_FILTER_DELETION_CAP
TAB_JUMP_FILTER_SCORE_THRESHOLD
TAB_JUMP_FILTER_WHITESPACE_ONLY
TAB_JUMP_FILTER_NO_OP
TAB_JUMP_FILTER_REVERT
TAB_JUMP_STOP_TOKEN_MIDSTREAM
TAB_JUMP_ON_ACCEPT_ONLY
TAB_JUMP_PRUNE_RESPONSE
TAB_JUMP_PRUNE_MAX_INSERT_DELETE_LINE_DELTA
TAB_JUMP_MIN_FILTER_RADIUS
```
---
## Browser Automation Policies
### JS Execution Policy (user can control)
```
BROWSER_JS_EXECUTION_POLICY_UNSPECIFIED
BROWSER_JS_EXECUTION_POLICY_ALWAYS_ASK
BROWSER_JS_EXECUTION_POLICY_MODEL_DECIDES
BROWSER_JS_EXECUTION_POLICY_TURBO — never ask
BROWSER_JS_EXECUTION_POLICY_DISABLED
```
### JS Auto-Run Policy (cascade-level)
```
BROWSER_JS_AUTO_RUN_POLICY_UNSPECIFIED
BROWSER_JS_AUTO_RUN_POLICY_ENABLED
BROWSER_JS_AUTO_RUN_POLICY_DISABLED
BROWSER_JS_AUTO_RUN_POLICY_MODEL_DECIDES
```
### Browser Subagent Mode
```
BROWSER_SUBAGENT_MODE_UNSPECIFIED
BROWSER_SUBAGENT_MODE_MAIN_AGENT_ONLY
BROWSER_SUBAGENT_MODE_SUBAGENT_ONLY
BROWSER_SUBAGENT_MODE_SUBAGENT_PRIMARILY
BROWSER_SUBAGENT_MODE_BOTH_AGENTS
```
### Tool Set Mode (input/output level)
```
BROWSER_TOOL_SET_MODE_UNSPECIFIED
BROWSER_TOOL_SET_MODE_ALL_TOOLS
BROWSER_TOOL_SET_MODE_PIXEL_ONLY
BROWSER_TOOL_SET_MODE_ALL_INPUT_PIXEL_OUTPUT
```
### Ephemeral Options (what gets captured)
```
BROWSER_EPHEMERAL_OPTION_UNSPECIFIED
BROWSER_EPHEMERAL_OPTION_DOM
BROWSER_EPHEMERAL_OPTION_SCREENSHOT
```
### Action Waiting Reasons
```
BROWSER_ACTION_WAITING_REASON_UNSPECIFIED
BROWSER_ACTION_WAITING_REASON_PAGE_ACCESS
BROWSER_ACTION_WAITING_REASON_ACTION_PERMISSION
BROWSER_ACTION_WAITING_REASON_PAGE_ACCESS_AND_ACTION_PERMISSION
```
### Installation Status
```
BROWSER_INSTALLATION_STATUS_UNSPECIFIED
BROWSER_INSTALLATION_STATUS_NOT_INSTALLED
BROWSER_INSTALLATION_STATUS_IN_PROGRESS
BROWSER_INSTALLATION_STATUS_COMPLETE
BROWSER_INSTALLATION_STATUS_ERROR
```
---
## Sandbox System (macOS Seatbelt)
The LS includes a `sandbox-wrapper.sh` that sandboxes command execution on macOS using Apple's Seatbelt framework.
### Key Behaviors
- **File writes**: Denied by default, allowed only to workspace dir and `/tmp`
- **Network**: Denied unless `--allow-network` flag is passed
- **Gitignore**: Reads `.gitignore` and `.agyignore` to deny access to ignored files
- **Process isolation**: Uses `sandbox-exec` with a generated Seatbelt profile
- **Violation detection**: Detects "Operation not permitted" errors and suggests disabling sandbox
### `.agyignore` File
Custom ignore file (like `.gitignore`) specifically for the sandbox. Patterns in `.agyignore` deny both read and write access within the sandbox.
---
## Trainer / RLHF Infrastructure
The binary contains extensive proto definitions for model training, suggesting the LS participates in or configures training runs.
### Proto Package: `exa.trainer_pb`
**Training Objectives:**
```
DPOConfig — Direct Preference Optimization
KTOConfig — Kahneman-Tversky Optimization
OnPolicyGRPOConfig — Group Relative Policy Optimization
KnowledgeDistillationConfig
PreferenceLoss — preference-based loss functions
```
**Model Architecture:**
```
BaseModelConfig
EncoderDecoderModelConfig
MixtureOfExpertsConfig — MoE architecture
MultiLatentAttentionConfig — MLA (DeepSeek-style?)
MultiTokenPredictionConfig
CrossAttentionConfig
SlidingWindowAttentionConfig
```
**Quantization:**
```
QuantizationConfig
QuantizationMethod
QuantizationPrecision
QuantizationPolicy / PolicySet
LinearQuantization
LayerNormQuantization
```
**Training Infrastructure:**
```
OptimizerType / OptimizerExpAvgDtype
CheckpointBackend (TORCH_DCP, ZARR)
ParallelismConfig / LayerParallelismConfig
RoPEEmbeddingConfig (Llama, DeepseekV scaling)
SGLangInferenceProviderConfig
MockedInferenceProviderConfig
```
**Experiment Management:**
```
ExperimentProject / ExperimentMetadata
SweepAxis / SweepItem
ScheduledModel / SchedulingGroupConfig
DeviceWorkerRoutingInfo
DraftTargetModelPair — speculative decoding?
```
### Key Proto Packages
```
exa.cortex_pb — agent/cascade core
exa.chat_pb — chat message/conversation protos
exa.trainer_pb — training/RLHF
exa.analytics_pb — telemetry
exa.api_server_pb — cloud API server
exa.seat_management_pb — user/team management
exa.user_analytics_pb — user analytics
exa.opensearch_clients_pb — knowledge base / code index
exa.language_server_pb — LS service
exa.extension_server_pb — extension server
exa.model_management_pb — model management
```
---
## MCP Integration Constants
```
MCP_SERVER_STATUS_UNSPECIFIED
MCP_SERVER_STATUS_PENDING
MCP_SERVER_STATUS_READY
MCP_SERVER_STATUS_ERROR
MCP_ADD_SERVER
MCP_SAVE_CONFIG
MCP_EXPAND_TOOLS
MCP_TOOL
MCP_RESOURCE
```
---
## Cortex Step Statuses
```
CORTEX_STEP_STATUS_UNSPECIFIED
CORTEX_STEP_STATUS_PENDING
CORTEX_STEP_STATUS_QUEUED
CORTEX_STEP_STATUS_GENERATING
CORTEX_STEP_STATUS_RUNNING
CORTEX_STEP_STATUS_WAITING
CORTEX_STEP_STATUS_DONE
CORTEX_STEP_STATUS_ERROR
CORTEX_STEP_STATUS_CANCELED
CORTEX_STEP_STATUS_CLEARED
CORTEX_STEP_STATUS_HALTED
CORTEX_STEP_STATUS_INTERRUPTED
CORTEX_STEP_STATUS_INVALID
```
### Step Sources
```
CORTEX_STEP_SOURCE_UNSPECIFIED
CORTEX_STEP_SOURCE_MODEL — generated by LLM
CORTEX_STEP_SOURCE_SYSTEM — system-generated
CORTEX_STEP_SOURCE_SYSTEM_SDK — SDK-generated
CORTEX_STEP_SOURCE_USER_EXPLICIT — user-initiated
CORTEX_STEP_SOURCE_USER_IMPLICIT — implied by user action
```
### Step Credit Reasons
```
CORTEX_STEP_CREDIT_REASON_UNSPECIFIED
CORTEX_STEP_CREDIT_REASON_LINT_FIXING_DISCOUNT — lint fixes are discounted
```
### Manager Feedback Status
```
CORTEX_STEP_MANAGER_FEEDBACK_STATUS_UNSPECIFIED
CORTEX_STEP_MANAGER_FEEDBACK_STATUS_APPROVED
CORTEX_STEP_MANAGER_FEEDBACK_STATUS_DENIED
CORTEX_STEP_MANAGER_FEEDBACK_STATUS_ERROR
```
### Error Categories
```
CORTEX_ERROR_CATEGORY_UNSPECIFIED
CORTEX_ERROR_CATEGORY_OVERALL
CORTEX_ERROR_CATEGORY_ACTION_PREPARE
CORTEX_ERROR_CATEGORY_ACTION_APPLY
```
### Compile Tools (Lint Integration)
```
CORTEX_STEP_COMPILE_TOOL_UNSPECIFIED
CORTEX_STEP_COMPILE_TOOL_PYLINT
```
---
## Onboarding Flow
```
ONBOARDING_PROGRESS_UNKNOWN
ONBOARDING_PROGRESS_NONE
ONBOARDING_PROGRESS_ELIGIBLE
ONBOARDING_PROGRESS_ONBOARDED
ONBOARDING_ACTION_TYPE_UNSPECIFIED
ONBOARDING_ACTION_TYPE_CHAT
ONBOARDING_ACTION_TYPE_AUTOCOMPLETE
ONBOARDING_ACTION_TYPE_COMMAND
ONBOARDING_PAGE_TYPE_UNSPECIFIED
ONBOARDING_PAGE_TYPE_HOTKEY
ONBOARDING_PAGE_TYPE_LENS
ONBOARDING_PAGE_TYPE_SEARCH
```
**Onboarding pages discovered:**
```
ONBOARDING_LANDING_PAGE_OPENED
ONBOARDING_AUTH_PAGE_OPENED
ONBOARDING_AUTH_MANUAL_PAGE_OPENED
ONBOARDING_CHOOSE_THEME_PAGE_OPENED
ONBOARDING_KEYBINDINGS_PAGE_OPENED
ONBOARDING_AGENT_CONFIG_PAGE_OPENED
ONBOARDING_IMPORT_PAGE_OPENED
ONBOARDING_MIGRATION_SCOPE_PAGE_OPENED
ONBOARDING_SETUP_PAGE_OPENED
ONBOARDING_TERMS_OF_USE_PAGE_OPENED
ONBOARDING_USAGE_MODE_PAGE_OPENED
```
---
## Protobuf Enum Numbers (from extension.js) ## Protobuf Enum Numbers (from extension.js)
Extracted from the compiled protobuf enum definitions in `extension.js`. Extracted from the compiled protobuf enum definitions in `extension.js`.

View File

@@ -1,7 +1,7 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# ZeroGravity — Linux setup # ZeroGravity — Linux setup
# Creates the zerogravity-ls system user for UID-scoped iptables isolation, # Checks prerequisites, creates the zerogravity-ls system user for
# installs the systemd user service, and builds the dns_redirect.so preload lib. # UID-scoped iptables isolation, and builds the release binary.
set -euo pipefail set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
@@ -18,7 +18,26 @@ if [ ${#MISSING[@]} -gt 0 ]; then
exit 1 exit 1
fi fi
# ── 1. System user for UID isolation ── # ── 1. Prerequisite check: Antigravity must be installed ──
LS_BINARY="${ZEROGRAVITY_LS_PATH:-/usr/share/antigravity/resources/app/extensions/antigravity/bin/language_server_linux_x64}"
echo "→ Checking for Antigravity installation…"
if [ ! -f "$LS_BINARY" ]; then
echo ""
echo "✗ Antigravity is not installed (or the LS binary is missing)."
echo " ZeroGravity requires a working Antigravity installation."
echo " The Language Server binary is bundled with the Antigravity app"
echo " and cannot be downloaded separately."
echo ""
echo " Expected path:"
echo " $LS_BINARY"
echo ""
echo " Install Antigravity first, then re-run this script."
echo " Alternatively, set ZEROGRAVITY_LS_PATH to a custom LS binary location."
exit 1
fi
echo " Found: $LS_BINARY"
# ── 2. System user for UID isolation ──
echo "→ Creating zerogravity-ls system user…" echo "→ Creating zerogravity-ls system user…"
if id -u zerogravity-ls &>/dev/null; then if id -u zerogravity-ls &>/dev/null; then
echo " Already exists." echo " Already exists."
@@ -27,7 +46,7 @@ else
echo " Created." echo " Created."
fi fi
# ── 2. Sudoers rule (run commands as zerogravity-ls without password) ── # ── 3. Sudoers rule (run commands as zerogravity-ls without password) ──
SUDOERS="/etc/sudoers.d/zerogravity" SUDOERS="/etc/sudoers.d/zerogravity"
echo "→ Installing sudoers rule…" echo "→ Installing sudoers rule…"
if [ -f "$SUDOERS" ]; then if [ -f "$SUDOERS" ]; then
@@ -38,41 +57,15 @@ else
echo " Installed: $SUDOERS" echo " Installed: $SUDOERS"
fi fi
# ── 3. Data directory permissions ── # ── 4. Data directory permissions ──
echo "→ Setting up /tmp/zerogravity-standalone…" echo "→ Setting up /tmp/zerogravity-standalone…"
sudo mkdir -p /tmp/zerogravity-standalone sudo mkdir -p /tmp/zerogravity-standalone
sudo chmod 1777 /tmp/zerogravity-standalone sudo chmod 1777 /tmp/zerogravity-standalone
# ── 4. Config directory ── # ── 5. Config directory ──
echo "→ Setting up ~/.config/zerogravity…" echo "→ Setting up ~/.config/zerogravity…"
mkdir -p "$HOME/.config/zerogravity" mkdir -p "$HOME/.config/zerogravity"
# ── 5. Systemd user service ──
echo "→ Installing systemd user service…"
UNIT_DIR="$HOME/.config/systemd/user"
mkdir -p "$UNIT_DIR"
cat > "$UNIT_DIR/zerogravity.service" << EOF
[Unit]
Description=ZeroGravity Proxy
After=network.target
[Service]
Type=simple
ExecStart=$PROJECT_DIR/target/release/zerogravity
WorkingDirectory=$PROJECT_DIR
Environment=RUST_LOG=info
Restart=on-failure
RestartSec=3
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=default.target
EOF
systemctl --user daemon-reload
echo " Installed: $UNIT_DIR/zerogravity.service"
echo " Enable with: systemctl --user enable zerogravity"
# ── 6. Build ── # ── 6. Build ──
echo "→ Building release binary…" echo "→ Building release binary…"
cd "$PROJECT_DIR" cd "$PROJECT_DIR"

View File

@@ -1,6 +1,6 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# ZeroGravity — macOS setup # ZeroGravity — macOS setup
# Installs a launchd plist for automatic startup and sets up config directories. # Checks prerequisites, sets up config directories, and builds the release binary.
# No UID isolation on macOS — runs in headless/HTTPS_PROXY mode only. # No UID isolation on macOS — runs in headless/HTTPS_PROXY mode only.
set -euo pipefail set -euo pipefail
@@ -16,50 +16,37 @@ mkdir -p "$CONFIG_DIR"
echo "→ Setting up /tmp/zerogravity-standalone…" echo "→ Setting up /tmp/zerogravity-standalone…"
mkdir -p /tmp/zerogravity-standalone mkdir -p /tmp/zerogravity-standalone
# ── 3. Launchd plist ── # ── 3. Prerequisite check: Antigravity must be installed ──
echo "→ Installing launchd plist…" LS_BINARY="${ZEROGRAVITY_LS_PATH:-}"
PLIST_DIR="$HOME/Library/LaunchAgents" if [ -z "$LS_BINARY" ]; then
PLIST="$PLIST_DIR/com.zerogravity.proxy.plist" # Check both /Applications and ~/Applications
mkdir -p "$PLIST_DIR" for base in "/Applications/Antigravity.app" "$HOME/Applications/Antigravity.app"; do
cat > "$PLIST" << EOF candidate="$base/Contents/Resources/app/extensions/antigravity/bin/language_server_darwin_arm64"
<?xml version="1.0" encoding="UTF-8"?> if [ -f "$candidate" ]; then
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> LS_BINARY="$candidate"
<plist version="1.0"> break
<dict> fi
<key>Label</key> done
<string>com.zerogravity.proxy</string> fi
<key>ProgramArguments</key> echo "→ Checking for Antigravity installation…"
<array> if [ -z "$LS_BINARY" ] || [ ! -f "$LS_BINARY" ]; then
<string>$PROJECT_DIR/target/release/zerogravity</string> echo ""
</array> echo "✗ Antigravity is not installed (or the LS binary is missing)."
<key>WorkingDirectory</key> echo " ZeroGravity requires a working Antigravity installation."
<string>$PROJECT_DIR</string> echo " The Language Server binary is bundled with the Antigravity app"
<key>EnvironmentVariables</key> echo " and cannot be downloaded separately."
<dict> echo ""
<key>RUST_LOG</key> echo " Expected in: /Applications/Antigravity.app or ~/Applications/Antigravity.app"
<string>info</string> echo ""
</dict> echo " Install Antigravity first, then re-run this script."
<key>KeepAlive</key> echo " Alternatively, set ZEROGRAVITY_LS_PATH to a custom LS binary location."
<dict> exit 1
<key>SuccessfulExit</key> fi
<false/> echo " Found: $LS_BINARY"
</dict>
<key>StandardOutPath</key>
<string>$HOME/Library/Logs/zerogravity.log</string>
<key>StandardErrorPath</key>
<string>$HOME/Library/Logs/zerogravity.log</string>
</dict>
</plist>
EOF
echo " Installed: $PLIST"
echo " Start with: launchctl load $PLIST"
echo " Stop with: launchctl unload $PLIST"
# ── 4. Build ── # ── 4. Build ──
echo "→ Building release binary…" echo "→ Building release binary…"
cd "$PROJECT_DIR" cd "$PROJECT_DIR"
cargo build --release 2>&1 | tail -1 cargo build --release 2>&1 | tail -1
echo "" echo ""
echo "✓ Setup complete." echo "✓ Setup complete. Start with: zg start"
echo " Start with: launchctl load $PLIST"
echo " Or manually: zg start"

View File

@@ -1,6 +1,5 @@
# ZeroGravity — Windows setup # ZeroGravity — Windows setup
# Creates config directories, builds the release binary, and optionally # Creates config directories, checks prerequisites, and builds the release binary.
# installs as a scheduled task for automatic startup.
# Run as: powershell -ExecutionPolicy Bypass -File scripts\setup-windows.ps1 # Run as: powershell -ExecutionPolicy Bypass -File scripts\setup-windows.ps1
$ErrorActionPreference = "Stop" $ErrorActionPreference = "Stop"
@@ -19,7 +18,26 @@ $DataDir = Join-Path $env:TEMP "zerogravity-standalone"
New-Item -ItemType Directory -Force -Path $DataDir | Out-Null New-Item -ItemType Directory -Force -Path $DataDir | Out-Null
Write-Host " $DataDir" Write-Host " $DataDir"
# ── 3. Build ── # ── 3. Prerequisite check: Antigravity must be installed ──
Write-Host "→ Checking for Antigravity installation…"
$LsBinary = Join-Path $env:LOCALAPPDATA "Programs\Antigravity\resources\app\extensions\antigravity\bin\language_server_windows_x64.exe"
if (-not (Test-Path $LsBinary)) {
Write-Host ""
Write-Host "✗ Antigravity is not installed (or the LS binary is missing)." -ForegroundColor Red
Write-Host " ZeroGravity requires a working Antigravity installation."
Write-Host " The Language Server binary is bundled with the Antigravity app"
Write-Host " and cannot be downloaded separately."
Write-Host ""
Write-Host " Expected path:"
Write-Host " $LsBinary"
Write-Host ""
Write-Host " Install Antigravity first, then re-run this script."
Write-Host " Alternatively, set ZEROGRAVITY_LS_PATH to a custom LS binary location."
exit 1
}
Write-Host " Found: $LsBinary"
# ── 4. Build ──
Write-Host "→ Building release binary…" Write-Host "→ Building release binary…"
Push-Location $ProjectDir Push-Location $ProjectDir
cargo build --release cargo build --release
@@ -32,37 +50,6 @@ if (-not (Test-Path $Binary)) {
} }
Write-Host " Built: $Binary" Write-Host " Built: $Binary"
# ── 4. Scheduled task (optional) ──
$TaskName = "ZeroGravity Proxy"
$Existing = Get-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue
if ($Existing) {
Write-Host "→ Scheduled task '$TaskName' already exists. Updating…"
Unregister-ScheduledTask -TaskName $TaskName -Confirm:$false
}
Write-Host "→ Creating scheduled task '$TaskName'…"
$Action = New-ScheduledTaskAction `
-Execute $Binary `
-WorkingDirectory $ProjectDir
$Trigger = New-ScheduledTaskTrigger -AtLogOn
$Settings = New-ScheduledTaskSettingsSet `
-AllowStartIfOnBatteries `
-DontStopIfGoingOnBatteries `
-RestartCount 3 `
-RestartInterval (New-TimeSpan -Minutes 1)
Register-ScheduledTask `
-TaskName $TaskName `
-Action $Action `
-Trigger $Trigger `
-Settings $Settings `
-Description "ZeroGravity OpenAI-compatible proxy" | Out-Null
Write-Host " Installed as logon task."
Write-Host "" Write-Host ""
Write-Host "✓ Setup complete." Write-Host "✓ Setup complete."
Write-Host " Start now: schtasks /run /tn '$TaskName'" Write-Host " Start: .\target\release\zerogravity.exe"
Write-Host " Stop: schtasks /end /tn '$TaskName'"
Write-Host " Or manually: .\target\release\zerogravity.exe"

View File

@@ -325,9 +325,7 @@ fn svc_stop() -> bool {
} }
#[cfg(not(windows))] #[cfg(not(windows))]
{ {
let _ = Command::new("pkill") let _ = Command::new("pkill").args(["-f", "zerogravity"]).status();
.args(["-f", "zerogravity"])
.status();
} }
true true
} }
@@ -517,9 +515,8 @@ fn do_test(msg: &str) {
.replace('\n', "\\n") .replace('\n', "\\n")
.replace('\r', "\\r") .replace('\r', "\\r")
.replace('\t', "\\t"); .replace('\t', "\\t");
let body = format!( let body =
r#"{{"model":"gemini-3-flash","input":"{escaped}","stream":false,"timeout":30}}"# format!(r#"{{"model":"gemini-3-flash","input":"{escaped}","stream":false,"timeout":30}}"#);
);
match curl_post("/v1/responses", &body) { match curl_post("/v1/responses", &body) {
Some(json) => jq_print(&json), Some(json) => jq_print(&json),
None => { None => {

View File

@@ -42,15 +42,15 @@ use tracing::{debug, info, trace, warn};
/// We mirror this by maintaining a single upstream connection per domain. /// We mirror this by maintaining a single upstream connection per domain.
struct UpstreamPool { struct UpstreamPool {
domain: String, domain: String,
tls_config: Arc<rustls::ClientConfig>, tls_connector: boring2::ssl::SslConnector,
sender: Mutex<Option<hyper::client::conn::http2::SendRequest<Full<Bytes>>>>, sender: Mutex<Option<hyper::client::conn::http2::SendRequest<Full<Bytes>>>>,
} }
impl UpstreamPool { impl UpstreamPool {
fn new(domain: String, tls_config: Arc<rustls::ClientConfig>) -> Self { fn new(domain: String, tls_connector: boring2::ssl::SslConnector) -> Self {
Self { Self {
domain, domain,
tls_config, tls_connector,
sender: Mutex::new(None), sender: Mutex::new(None),
} }
} }
@@ -82,17 +82,29 @@ impl UpstreamPool {
.await .await
.map_err(|e| format!("upstream TCP connect to {} failed: {e}", self.domain))?; .map_err(|e| format!("upstream TCP connect to {} failed: {e}", self.domain))?;
let connector = tokio_rustls::TlsConnector::from(self.tls_config.clone()); let ssl = self
let server_name = rustls::pki_types::ServerName::try_from(self.domain.clone()) .tls_connector
.map_err(|e| format!("invalid domain {}: {e}", self.domain))?; .configure()
.map_err(|e| format!("SSL configure: {e}"))?
.into_ssl(&self.domain)
.map_err(|e| format!("SSL into_ssl: {e}"))?;
let upstream_tls = connector let mut upstream_tls = tokio_boring2::SslStream::new(ssl, upstream_tcp)
.connect(server_name, upstream_tcp)
.await
.map_err(|e| format!("upstream TLS to {} failed: {e}", self.domain))?; .map_err(|e| format!("upstream TLS to {} failed: {e}", self.domain))?;
std::pin::Pin::new(&mut upstream_tls)
.connect()
.await
.map_err(|e| format!("TLS handshake to {} failed: {e}", self.domain))?;
let upstream_io = TokioIo::new(upstream_tls); let upstream_io = TokioIo::new(upstream_tls);
// Configure HTTP/2 SETTINGS to match Go's net/http2 defaults
// Source: golang.org/x/net/http2/transport.go
let (sender, conn) = hyper::client::conn::http2::Builder::new(TokioExecutor::new()) let (sender, conn) = hyper::client::conn::http2::Builder::new(TokioExecutor::new())
.initial_stream_window_size(4 << 20) // 4MB (Go: transportDefaultStreamFlow)
.initial_connection_window_size(1 << 30) // 1GB (Go: transportDefaultConnFlow)
.max_header_list_size(10 * 1024 * 1024) // 10MB (Go: defaultMaxHeaderListSize)
.adaptive_window(false) // Go doesn't use adaptive windowing
.handshake(upstream_io) .handshake(upstream_io)
.await .await
.map_err(|e| format!("upstream h2 handshake to {} failed: {e}", self.domain))?; .map_err(|e| format!("upstream h2 handshake to {} failed: {e}", self.domain))?;
@@ -140,22 +152,11 @@ where
{ {
info!(domain = %domain, "MITM H2: handling HTTP/2 connection"); info!(domain = %domain, "MITM H2: handling HTTP/2 connection");
// Build TLS config for upstream connections // Build upstream TLS connector matching Go's crypto/tls fingerprint (with ALPN h2)
let mut root_store = rustls::RootCertStore::empty(); let upstream_connector = super::tls::build_go_tls_connector(Some(&[b"h2"]));
let native_certs = rustls_native_certs::load_native_certs();
for cert in native_certs.certs {
let _ = root_store.add(cert);
}
let mut upstream_tls_config = rustls::ClientConfig::builder()
.with_root_certificates(root_store)
.with_no_client_auth();
upstream_tls_config.alpn_protocols = vec![b"h2".to_vec()];
// Shared upstream connection pool (single connection, multiplexed) // Shared upstream connection pool (single connection, multiplexed)
let pool = Arc::new(UpstreamPool::new( let pool = Arc::new(UpstreamPool::new(domain.clone(), upstream_connector));
domain.clone(),
Arc::new(upstream_tls_config),
));
let io = TokioIo::new(tls_stream); let io = TokioIo::new(tls_stream);
let domain = Arc::new(domain); let domain = Arc::new(domain);

View File

@@ -18,3 +18,4 @@ pub mod modify;
pub mod proto; pub mod proto;
pub mod proxy; pub mod proxy;
pub mod store; pub mod store;
pub mod tls;

View File

@@ -368,20 +368,11 @@ async fn handle_http_over_tls(
) -> Result<(), String> { ) -> Result<(), String> {
let mut tmp = vec![0u8; 32768]; let mut tmp = vec![0u8; 32768];
// Build upstream TLS connector once for this connection // Build upstream TLS connector matching Go's crypto/tls fingerprint
let mut root_store = rustls::RootCertStore::empty(); let upstream_connector = super::tls::build_go_tls_connector(None);
let native_certs = rustls_native_certs::load_native_certs();
for cert in native_certs.certs {
let _ = root_store.add(cert);
}
let upstream_config = Arc::new(
rustls::ClientConfig::builder()
.with_root_certificates(root_store)
.with_no_client_auth(),
);
// Reusable upstream connection — created lazily, reconnected if stale // Reusable upstream connection — created lazily, reconnected if stale
let mut upstream: Option<tokio_rustls::client::TlsStream<TcpStream>> = None; let mut upstream: Option<tokio_boring2::SslStream<TcpStream>> = None;
// Keep-alive loop: handle multiple requests on this connection // Keep-alive loop: handle multiple requests on this connection
loop { loop {
@@ -575,7 +566,7 @@ async fn handle_http_over_tls(
let conn = match upstream.as_mut() { let conn = match upstream.as_mut() {
Some(c) => c, Some(c) => c,
None => { None => {
let c = connect_upstream(domain, &upstream_config).await?; let c = connect_upstream(domain, &upstream_connector).await?;
upstream.insert(c) upstream.insert(c)
} }
}; };
@@ -583,7 +574,7 @@ async fn handle_http_over_tls(
// Forward the request — if write fails, reconnect and retry once // Forward the request — if write fails, reconnect and retry once
if let Err(e) = conn.write_all(&request_buf).await { if let Err(e) = conn.write_all(&request_buf).await {
debug!(domain, error = %e, "MITM: upstream write failed, reconnecting"); debug!(domain, error = %e, "MITM: upstream write failed, reconnecting");
let c = connect_upstream(domain, &upstream_config).await?; let c = connect_upstream(domain, &upstream_connector).await?;
let conn = upstream.insert(c); let conn = upstream.insert(c);
conn.write_all(&request_buf) conn.write_all(&request_buf)
.await .await
@@ -905,15 +896,15 @@ async fn read_full_request(
/// Connect (or reconnect) to the real upstream via TLS. /// Connect (or reconnect) to the real upstream via TLS.
/// ///
/// Uses BoringSSL configured to match Go's `crypto/tls` fingerprint.
/// Bypasses /etc/hosts by resolving via direct DNS query (dig @8.8.8.8), /// Bypasses /etc/hosts by resolving via direct DNS query (dig @8.8.8.8),
/// then falls back to cached IPs file, then to normal system resolution. /// then falls back to cached IPs file, then to normal system resolution.
async fn connect_upstream( async fn connect_upstream(
domain: &str, domain: &str,
config: &Arc<rustls::ClientConfig>, connector: &boring2::ssl::SslConnector,
) -> Result<tokio_rustls::client::TlsStream<TcpStream>, String> { ) -> Result<tokio_boring2::SslStream<TcpStream>, String> {
let connector = tokio_rustls::TlsConnector::from(config.clone());
let addr = resolve_upstream(domain).await; let addr = resolve_upstream(domain).await;
info!(domain, addr = %addr, "MITM: connecting upstream"); info!(domain, addr = %addr, "MITM: connecting upstream (BoringSSL)");
let tcp = match tokio::time::timeout( let tcp = match tokio::time::timeout(
std::time::Duration::from_secs(15), std::time::Duration::from_secs(15),
@@ -926,20 +917,26 @@ async fn connect_upstream(
Err(_) => return Err(format!("Connect to upstream {domain} ({addr}): timed out")), Err(_) => return Err(format!("Connect to upstream {domain} ({addr}): timed out")),
}; };
let server_name = rustls::pki_types::ServerName::try_from(domain.to_string()) let ssl = connector
.map_err(|e| format!("Invalid server name: {e}"))?; .configure()
.map_err(|e| format!("SSL configure: {e}"))?
.into_ssl(domain)
.map_err(|e| format!("SSL into_ssl: {e}"))?;
let mut stream = tokio_boring2::SslStream::new(ssl, tcp)
.map_err(|e| format!("SslStream::new for {domain}: {e}"))?;
match tokio::time::timeout( match tokio::time::timeout(
std::time::Duration::from_secs(15), std::time::Duration::from_secs(15),
connector.connect(server_name, tcp), std::pin::Pin::new(&mut stream).connect(),
) )
.await .await
{ {
Ok(Ok(s)) => { Ok(Ok(())) => {
info!(domain, "MITM: upstream TLS connected ✓"); info!(domain, "MITM: upstream TLS connected ✓ (BoringSSL)");
Ok(s) Ok(stream)
} }
Ok(Err(e)) => Err(format!("TLS connect to upstream {domain}: {e}")), Ok(Err(e)) => Err(format!("TLS handshake to upstream {domain}: {e}")),
Err(_) => Err(format!("TLS connect to upstream {domain}: timed out")), Err(_) => Err(format!("TLS connect to upstream {domain}: timed out")),
} }
} }

86
src/mitm/tls.rs Normal file
View File

@@ -0,0 +1,86 @@
//! Upstream TLS configuration matching Go's `crypto/tls` defaults.
//!
//! The LS is a Go binary — its outbound TLS to Google uses Go's default
//! cipher suites, curves, and signature algorithms. This module configures
//! BoringSSL to produce a matching TLS ClientHello so Google sees the same
//! JA3/JA4 fingerprint regardless of whether our MITM is active.
use boring2::ssl::{SslConnector, SslMethod};
use tracing::debug;
/// Go's default cipher suites in the exact order Go's `crypto/tls` sends them.
///
/// TLS 1.3 ciphers (hardcoded in Go, not configurable):
/// TLS_AES_128_GCM_SHA256, TLS_AES_256_GCM_SHA384, TLS_CHACHA20_POLY1305_SHA256
///
/// TLS 1.2 ciphers (Go's default preference order):
/// ECDHE-ECDSA-AES128-GCM-SHA256, ECDHE-RSA-AES128-GCM-SHA256,
/// ECDHE-ECDSA-AES256-GCM-SHA384, ECDHE-RSA-AES256-GCM-SHA384,
/// ECDHE-ECDSA-CHACHA20-POLY1305, ECDHE-RSA-CHACHA20-POLY1305,
/// ECDHE-RSA-AES128-SHA, ECDHE-RSA-AES256-SHA,
/// AES128-GCM-SHA256, AES256-GCM-SHA384, AES128-SHA, AES256-SHA
const GO_CIPHER_LIST: &str = "\
TLS_AES_128_GCM_SHA256:\
TLS_AES_256_GCM_SHA384:\
TLS_CHACHA20_POLY1305_SHA256:\
ECDHE-ECDSA-AES128-GCM-SHA256:\
ECDHE-RSA-AES128-GCM-SHA256:\
ECDHE-ECDSA-AES256-GCM-SHA384:\
ECDHE-RSA-AES256-GCM-SHA384:\
ECDHE-ECDSA-CHACHA20-POLY1305:\
ECDHE-RSA-CHACHA20-POLY1305:\
ECDHE-RSA-AES128-SHA:\
ECDHE-RSA-AES256-SHA:\
AES128-GCM-SHA256:\
AES256-GCM-SHA384:\
AES128-SHA:\
AES256-SHA";
/// Go's default signature algorithms.
const GO_SIGALGS: &str = "\
ECDSA+SHA256:\
RSA-PSS+SHA256:\
RSA+SHA256:\
ECDSA+SHA384:\
RSA-PSS+SHA384:\
RSA+SHA384:\
RSA-PSS+SHA512:\
RSA+SHA512:\
RSA+SHA1";
/// Build an `SslConnector` that mimics Go's `crypto/tls` defaults.
///
/// If `alpn` is provided, sets ALPN protocols (e.g. `&[b"h2"]` for HTTP/2).
pub fn build_go_tls_connector(alpn: Option<&[&[u8]]>) -> SslConnector {
let mut builder =
SslConnector::builder(SslMethod::tls_client()).expect("SslConnector::builder");
// Set Go's cipher list
builder
.set_cipher_list(GO_CIPHER_LIST)
.expect("set_cipher_list");
// Set Go's signature algorithms
builder
.set_sigalgs_list(GO_SIGALGS)
.expect("set_sigalgs_list");
// Set Go's default curves: X25519, P-256, P-384
// BoringSSL uses set_curves_list with colon-separated names
builder
.set_curves_list("X25519:P-256:P-384")
.expect("set_curves_list");
// ALPN if requested (for HTTP/2)
if let Some(protos) = alpn {
let mut wire = Vec::new();
for proto in protos {
wire.push(proto.len() as u8);
wire.extend_from_slice(proto);
}
builder.set_alpn_protos(&wire).expect("set_alpn_protos");
}
debug!("Built Go-matching TLS connector (BoringSSL)");
builder.build()
}

View File

@@ -275,22 +275,32 @@ impl StandaloneLS {
// With iptables, all outbound traffic is transparently redirected at the // With iptables, all outbound traffic is transparently redirected at the
// kernel level — setting HTTPS_PROXY on top causes double-proxying. // kernel level — setting HTTPS_PROXY on top causes double-proxying.
if headless || !platform::supports_uid_isolation() { if headless || !platform::supports_uid_isolation() {
// proxy_addr already includes the scheme (e.g. "http://127.0.0.1:8742")
env_vars.push(("HTTPS_PROXY".into(), mitm.proxy_addr.clone()));
env_vars.push(("HTTP_PROXY".into(), mitm.proxy_addr.clone()));
// LD_PRELOAD DNS redirect: hooks getaddrinfo() so Google API domains // LD_PRELOAD DNS redirect: hooks getaddrinfo() so Google API domains
// resolve to 127.0.0.1. Combined with the port-modified endpoint URL, // resolve to 127.0.0.1. Combined with the port-modified endpoint URL,
// this makes the LS connect to our MITM proxy for ALL API calls — // this makes the LS connect to our MITM proxy for ALL API calls —
// even the CodeAssistClient which has Proxy:nil hardcoded. // even the CodeAssistClient which has Proxy:nil hardcoded.
let so_path = build_dns_redirect_so(); let so_path = build_dns_redirect_so();
if let Some(so) = so_path { if let Some(ref so) = so_path {
info!(path = %so, "Enabling LD_PRELOAD DNS redirect for headless MITM"); info!(path = %so, "Enabling LD_PRELOAD DNS redirect for headless MITM");
env_vars.push(("LD_PRELOAD".into(), so)); env_vars.push(("LD_PRELOAD".into(), so.clone()));
env_vars.push(( env_vars.push((
"DNS_REDIRECT_LOG".into(), "DNS_REDIRECT_LOG".into(),
format!("{data_dir}/dns-redirect.log"), format!("{data_dir}/dns-redirect.log"),
)); ));
// Force Go binaries to use cgo (libc) DNS resolver instead of
// the pure-Go resolver. Without this, LD_PRELOAD getaddrinfo()
// hooks are bypassed because Go resolves DNS internally.
env_vars.push(("GODEBUG".into(), "netdns=cgo".into()));
}
// Only set HTTPS_PROXY as fallback when DNS redirect is NOT available.
// When DNS redirect IS active, HTTPS_PROXY is redundant and harmful:
// Go's net/http sends HTTP CONNECT through the proxy, but the MITM
// proxy expects direct TLS connections (SNI-based interception).
// This causes OAuth token refresh and other non-gRPC calls to fail.
if so_path.is_none() {
env_vars.push(("HTTPS_PROXY".into(), mitm.proxy_addr.clone()));
env_vars.push(("HTTP_PROXY".into(), mitm.proxy_addr.clone()));
} }
} }
} }