Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
37b6088
feat(ansible): add optional self-hosted MinIO storage with secure def…
niieani Feb 6, 2026
83809f2
docs(ios): add instructions on Apple Certificates
niieani Feb 6, 2026
77e6bcd
fix(ios): reconnection after changing the URL
niieani Feb 6, 2026
5afe0ea
fix(ansible): make k8s-only self-sufficient for k8s-manifests deploys
niieani Feb 7, 2026
a3339e6
fix(ansible): ensure Helm is installed
niieani Feb 7, 2026
1e1cfdd
feat: backend-managed Codex OAuth flow
niieani Feb 7, 2026
cf9f1f7
fix(ios): surface new-session create failures instead of silent dismi…
niieani Feb 7, 2026
08af6ac
fix(agent,control-plane): make interrupt paths terminate prompt lifec…
niieani Feb 7, 2026
24a06d8
feat(ios): add cmd+enter send shortcut in chat input
niieani Feb 7, 2026
3901819
fix(ios): prevent expose port sheet env crash
niieani Feb 7, 2026
7bc9b2a
fix(control-plane): return routable preview URLs and tighten ingress …
niieani Feb 7, 2026
46656ab
feat(ports): add end-to-end port unexpose support
niieani Feb 7, 2026
ce27671
fix(agent): reinitialize SDK adapters after stream reconnect
niieani Feb 7, 2026
5586482
fix(control-plane): prevent stale PVC resume deadlocks
niieani Feb 7, 2026
c6474a2
fix(control-plane): auto-restore latest snapshot when resume PVC is m…
niieani Feb 7, 2026
9e06374
fix(control-plane): avoid resume hangs when restore job is absent
niieani Feb 7, 2026
f8fbd38
feat(devflow): add fast remote build/deploy iteration workflow
niieani Feb 7, 2026
8702ec8
chore: update agent versions to latest versions
niieani Feb 9, 2026
b49bb32
feat(ios): improve model defaults and picker selection UX
niieani Feb 9, 2026
4cfedad
chore: update go.work.sum; follow up to f0306a2
niieani Feb 9, 2026
ab22b0d
fix(control-plane): make port unexpose idempotent on missing service
niieani Feb 9, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -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/
Expand Down
23 changes: 20 additions & 3 deletions .env.example
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -15,12 +23,21 @@ TS_OAUTH_CLIENT_SECRET=
# If not set, authenticate manually on the host: tailscale up --ssh
TAILSCALE_AUTHKEY=

# JuiceFS S3 backend (DigitalOcean Spaces)
# JuiceFS S3 backend (external S3 provider)
# Optional when MINIO_ENABLED=true (auto-wired from /var/secrets/minio-root-*)
DO_SPACES_ACCESS_KEY=
DO_SPACES_SECRET_KEY=
JUICEFS_BUCKET=https://nyc3.digitaloceanspaces.com/your-bucket
JUICEFS_META_URL=redis://localhost:6379/0

# Self-hosted MinIO (optional)
# Set MINIO_ENABLED=true, run ansible playbook with --tags minio
# MinIO can reuse DO_SPACES_ACCESS_KEY / DO_SPACES_SECRET_KEY as credentials
MINIO_ENABLED=false
# MINIO_BUCKET_NAME=netclode-juicefs
# MINIO_API_PORT=9000
# MINIO_CONSOLE_PORT=9001

# Deployment target (for deploy-secrets.sh, rollout.sh)
DEPLOY_HOST=root@your-server

Expand Down
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
97 changes: 93 additions & 4 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,7 +1,31 @@
CONTEXT ?= netclode
NAMESPACE ?= netclode
TAG ?= dev-$(shell date +%Y%m%d-%H%M%S)
DEV_CONTROL_PLANE_IMAGE ?= netclode-control-plane:$(TAG)
DEV_AGENT_IMAGE ?= netclode-agent:$(TAG)
ANSIBLE_EXTRA_ARGS ?=
ANSIBLE_USER ?=

.PHONY: rollout rollout-control-plane rollout-agent deploy test-ios run-macos run-ios run-device proto proto-lint proto-breaking proto-setup
TEAM_ID_AUTO ?= $(shell \
team=$$(security find-certificate -a -c "Apple Development" -p "$$HOME/Library/Keychains/login.keychain-db" 2>/dev/null | \
openssl x509 -noout -subject 2>/dev/null | sed -n 's/.*OU=\([^,]*\).*/\1/p' | head -n 1); \
if [ -z "$$team" ]; then \
profile=$$(ls -1 "$$HOME/Library/Developer/Xcode/UserData/Provisioning Profiles"/*.mobileprovision 2>/dev/null | head -n 1); \
if [ -n "$$profile" ]; then \
team=$$(security cms -D -i "$$profile" 2>/dev/null | \
awk 'found && /<string>/{gsub(/.*<string>|<\/string>.*/, ""); print; exit} /<key>TeamIdentifier<\/key>/{found=1}'); \
fi; \
fi; \
printf "%s" "$$team" \
)
TEAM_ID ?= $(TEAM_ID_AUTO)

XCODE_SIGN_ARGS := -allowProvisioningUpdates
ifneq ($(strip $(TEAM_ID)),)
XCODE_SIGN_ARGS += DEVELOPMENT_TEAM=$(TEAM_ID)
endif

.PHONY: rollout rollout-control-plane rollout-agent drain-warmpool deploy test-ios run-macos run-ios run-device print-ios-team-id proto proto-lint proto-breaking proto-setup dev-install-builder dev-build-remote dev-build-remote-control-plane dev-build-remote-agent dev-deploy-images dev-verify dev-loop-remote dev-loop-ansible dev-loop-ansible-build dev-loop-ansible-deploy dev-loop-ansible-verify

# Proto generation
proto: proto-setup ## Generate code from proto files
Expand Down Expand Up @@ -50,24 +74,89 @@ deploy: ## Wait for CI then rollout control-plane
gh run watch $$(gh run list --limit 1 --json databaseId --jq '.[0].databaseId') --exit-status
$(MAKE) rollout-control-plane

dev-build-remote: ## Build control-plane + agent images on DEPLOY_HOST and import into k3s containerd
@TAG=$(TAG) CONTROL_PLANE_IMAGE=$(DEV_CONTROL_PLANE_IMAGE) AGENT_IMAGE=$(DEV_AGENT_IMAGE) scripts/dev/build-on-remote.sh

dev-build-remote-control-plane: ## Build only control-plane image on DEPLOY_HOST and import into k3s containerd
@TAG=$(TAG) BUILD_AGENT=0 CONTROL_PLANE_IMAGE=$(DEV_CONTROL_PLANE_IMAGE) scripts/dev/build-on-remote.sh

dev-build-remote-agent: ## Build only agent image on DEPLOY_HOST and import into k3s containerd
@TAG=$(TAG) BUILD_CONTROL_PLANE=0 AGENT_IMAGE=$(DEV_AGENT_IMAGE) scripts/dev/build-on-remote.sh

dev-deploy-images: ## Fast dev deploy via kubectl (no Ansible): update images + AGENT_IMAGE env + refresh warm pool
@CONTROL_PLANE_IMAGE=$(DEV_CONTROL_PLANE_IMAGE) AGENT_IMAGE=$(DEV_AGENT_IMAGE) CONTEXT=$(CONTEXT) NAMESPACE=$(NAMESPACE) scripts/dev/deploy-dev-images.sh

dev-verify: ## Verify control-plane + sandbox template image wiring and logs
@CONTEXT=$(CONTEXT) NAMESPACE=$(NAMESPACE) scripts/dev/verify-dev-loop.sh

dev-loop-remote: ## Build on DEPLOY_HOST, deploy with kubectl patch, then verify (TAG=dev-...)
@$(MAKE) dev-build-remote TAG=$(TAG)
@$(MAKE) dev-deploy-images TAG=$(TAG)
@$(MAKE) dev-verify

dev-install-builder: ## Install Docker + Buildx on DEPLOY_HOST for remote dev builds
@set -a; [ -f .env ] && . ./.env || true; set +a; \
cd infra/ansible && ansible-playbook playbooks/dev-builder.yaml \
$(if $(strip $(ANSIBLE_USER)),-e ansible_user=$(ANSIBLE_USER),) \
$(ANSIBLE_EXTRA_ARGS)

dev-loop-ansible: ## Fast dev loop via Ansible (build + deploy + verify)
@set -a; [ -f .env ] && . ./.env || true; set +a; \
cd infra/ansible && ansible-playbook playbooks/dev-loop.yaml \
$(if $(strip $(ANSIBLE_USER)),-e ansible_user=$(ANSIBLE_USER),) \
-e k8s_namespace=$(NAMESPACE) \
-e tag=$(TAG) \
-e control_plane_image=$(DEV_CONTROL_PLANE_IMAGE) \
-e agent_image=$(DEV_AGENT_IMAGE) \
$(ANSIBLE_EXTRA_ARGS)

dev-loop-ansible-build: ## Ansible dev loop: build/import only
@set -a; [ -f .env ] && . ./.env || true; set +a; \
cd infra/ansible && ansible-playbook playbooks/dev-loop.yaml --tags dev-build \
$(if $(strip $(ANSIBLE_USER)),-e ansible_user=$(ANSIBLE_USER),) \
-e k8s_namespace=$(NAMESPACE) \
-e tag=$(TAG) \
-e control_plane_image=$(DEV_CONTROL_PLANE_IMAGE) \
-e agent_image=$(DEV_AGENT_IMAGE) \
$(ANSIBLE_EXTRA_ARGS)

dev-loop-ansible-deploy: ## Ansible dev loop: deploy/rollout only
@set -a; [ -f .env ] && . ./.env || true; set +a; \
cd infra/ansible && ansible-playbook playbooks/dev-loop.yaml --tags dev-deploy \
$(if $(strip $(ANSIBLE_USER)),-e ansible_user=$(ANSIBLE_USER),) \
-e k8s_namespace=$(NAMESPACE) \
-e control_plane_image=$(DEV_CONTROL_PLANE_IMAGE) \
-e agent_image=$(DEV_AGENT_IMAGE) \
$(ANSIBLE_EXTRA_ARGS)

dev-loop-ansible-verify: ## Ansible dev loop: verification only
@set -a; [ -f .env ] && . ./.env || true; set +a; \
cd infra/ansible && ansible-playbook playbooks/dev-loop.yaml --tags dev-verify \
$(if $(strip $(ANSIBLE_USER)),-e ansible_user=$(ANSIBLE_USER),) \
-e k8s_namespace=$(NAMESPACE) \
$(ANSIBLE_EXTRA_ARGS)

test-ios: ## Run iOS unit tests
cd clients/ios && xcodebuild test -scheme NetclodeTests -destination 'platform=macOS' -quiet

run-macos: ## Build and run macOS (Catalyst) app
cd clients/ios && xcodebuild -scheme Netclode -destination 'platform=macOS,variant=Mac Catalyst' -derivedDataPath .build build
cd clients/ios && xcodebuild -scheme Netclode -destination 'platform=macOS,variant=Mac Catalyst' -derivedDataPath .build $(XCODE_SIGN_ARGS) build
open clients/ios/.build/Build/Products/Debug-maccatalyst/Netclode.app

SIMULATOR ?= iPhone 16 Pro
run-ios: ## Build and run iOS simulator app (SIMULATOR="iPhone 16 Pro")
xcrun simctl boot "$(SIMULATOR)" 2>/dev/null || true
cd clients/ios && xcodebuild -scheme Netclode -destination 'platform=iOS Simulator,name=$(SIMULATOR)' -derivedDataPath .build build
cd clients/ios && xcodebuild -scheme Netclode -destination 'platform=iOS Simulator,name=$(SIMULATOR)' -derivedDataPath .build $(XCODE_SIGN_ARGS) build
xcrun simctl install "$(SIMULATOR)" clients/ios/.build/Build/Products/Debug-iphonesimulator/Netclode.app
xcrun simctl launch "$(SIMULATOR)" com.netclode.ios

run-device: ## Build and run on connected iPhone
cd clients/ios && xcodebuild -scheme Netclode -destination 'generic/platform=iOS' -derivedDataPath .build build
cd clients/ios && xcodebuild -scheme Netclode -destination 'generic/platform=iOS' -derivedDataPath .build $(XCODE_SIGN_ARGS) -allowProvisioningDeviceRegistration build
xcrun devicectl device install app --device "$(shell xcrun devicectl list devices 2>/dev/null | grep iPhone | grep -oE '[0-9A-F-]{36}' | head -1)" clients/ios/.build/Build/Products/Debug-iphoneos/Netclode.app
xcrun devicectl device process launch --device "$(shell xcrun devicectl list devices 2>/dev/null | grep iPhone | grep -oE '[0-9A-F-]{36}' | head -1)" com.netclode.ios

print-ios-team-id: ## Print detected iOS signing Team ID (override with TEAM_ID=...)
@echo $(TEAM_ID)

help: ## Show this help
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}'
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
27 changes: 17 additions & 10 deletions clients/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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:
```
Expand All @@ -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=<host> ansible-playbook playbooks/site.yaml
```bash
netclode auth codex status
```

Remove local OAuth tokens:

```bash
netclode auth codex logout
```

## Global Flags
Expand Down Expand Up @@ -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
Expand Down
Loading