diff --git a/.claude/aenvironment-deploy/SKILL.md b/.claude/aenvironment-deploy/SKILL.md new file mode 100644 index 0000000..b3b3cd4 --- /dev/null +++ b/.claude/aenvironment-deploy/SKILL.md @@ -0,0 +1,345 @@ +--- +name: aenvironment-deploy +description: Deploy sandboxed environment instances and services using AEnvironment. Use when deploying agent instances, web services, or applications to AEnvironment sandbox infrastructure. Supports three workflows - (1) Build image locally and deploy, (2) Register existing image and deploy, (3) Deploy from registered environment. Handles instance deployment (temporary, IP-based access for agents) and service deployment (persistent, domain-based access with storage for apps). +--- + +# AEnvironment Deploy + +Automate deployment of sandboxed environment instances and services using the AEnvironment platform. + +## Overview + +AEnvironment provides isolated sandbox environments for running agents and applications. This skill handles the complete deployment workflow: + +- **Instances**: Short-lived environments for agents (IP-based access, no persistence) +- **Services**: Long-running services for apps (domain access, optional storage, multiple replicas) + +## Prerequisites + +Install AEnvironment CLI: + +```bash +pip install aenvironment --upgrade +aenv --help +``` + +## Deployment Workflows + +### Workflow A: Deploy with Local Image Build + +Build Docker image locally, register to EnvHub, and deploy. + +**When to use**: Creating a new environment from scratch with local Dockerfile. + +**Requirements**: Docker installed, registry credentials configured. + +**Script**: `scripts/deploy_with_local_build.py` + +**Example**: + +```bash +python scripts/deploy_with_local_build.py \ + --env-name myagent \ + --owner-name john \ + --api-service-url https://api.example.com \ + --envhub-url https://envhub.example.com \ + --registry-host registry.example.com \ + --registry-username user \ + --registry-password pass \ + --registry-namespace myteam \ + --deploy-type instance \ + --ttl 24h \ + --env-vars '{"API_KEY":"xxx"}' +``` + +### Workflow B: Deploy with Existing Image + +Register existing Docker image to EnvHub and deploy. + +**When to use**: You have a pre-built Docker image to deploy. + +**Requirements**: Existing image accessible in registry. + +**Script**: `scripts/deploy_with_existing_image.py` + +**Example**: + +```bash +python scripts/deploy_with_existing_image.py \ + --env-name myagent \ + --image-name registry.example.com/myteam/agent:1.0.0 \ + --owner-name john \ + --api-service-url https://api.example.com \ + --envhub-url https://envhub.example.com \ + --deploy-type instance \ + --ttl 48h +``` + +### Workflow C: Deploy Existing Environment + +Deploy from already registered environment in EnvHub. + +**When to use**: Environment is already registered, just need to deploy. + +**Requirements**: Environment registered in EnvHub. + +**Script**: `scripts/deploy_existing_env.py` + +**Example**: + +```bash +python scripts/deploy_existing_env.py \ + --env-spec myagent@1.0.0 \ + --owner-name john \ + --api-service-url https://api.example.com \ + --envhub-url https://envhub.example.com \ + --deploy-type instance +``` + +## Instance vs Service + +| Feature | Instance | Service | +|---------|----------|---------| +| **Lifecycle** | Temporary (with TTL) | Permanent | +| **Access** | IP + port | Service domain | +| **Replicas** | Single | Multiple supported | +| **Storage** | No | Optional (requires replicas=1) | +| **Use Case** | Agents, dev/test | Production apps, APIs | + +## Common Parameters + +### Required (All Workflows) + +- `--env-name`: Environment name +- `--owner-name`: Resource owner identifier +- `--api-service-url`: AEnvironment API URL +- `--envhub-url`: EnvHub service URL +- `--deploy-type`: `instance` or `service` + +### Instance Options + +- `--ttl`: Time to live (default: "24h", examples: "30m", "48h", "100h") +- `--env-vars`: JSON dict of environment variables + +### Service Options + +- `--replicas`: Number of replicas (default: 1) +- `--port`: Service port (default: 8080) +- `--enable-storage`: Enable persistent storage (forces replicas=1) +- `--env-vars`: JSON dict of environment variables + +### Registry Options (Workflow A only) + +- `--registry-host`: Registry hostname +- `--registry-username`: Registry username +- `--registry-password`: Registry password +- `--registry-namespace`: Registry namespace + +## Configuration + +After initialization, edit `config.json` to customize: + +```json +{ + "name": "myenv", + "version": "1.0.0", + "artifacts": [ + { + "type": "image", + "content": "registry.example.com/myimage:latest" + } + ], + "requirements": { + "cpu": "1000m", + "memory": "2Gi" + }, + "deployConfig": { + "service": { + "replicas": 1, + "port": 8080, + "enableStorage": false, + "storageSize": "10Gi", + "mountPath": "/data" + } + } +} +``` + +**See**: [references/CONFIG_SCHEMA.md](references/CONFIG_SCHEMA.md) for complete schema reference. + +## Accessing Deployed Resources + +### Instance Access + +Instances provide IP-based access: + +```text +http://: +``` + +Check instance details: + +```bash +python -c "from scripts.aenv_operations import AEnvOperations; \ + ops = AEnvOperations(); \ + ops.configure_cli('owner', 'api-url', 'hub-url'); \ + print(ops.list_instances())" +``` + +### Service Access + +Services provide domain-based access: + +```text +http://.aenv-sandbox.svc.tydd-staging.alipay.net: +``` + +Check service details: + +```bash +python -c "from scripts.aenv_operations import AEnvOperations; \ + ops = AEnvOperations(); \ + ops.configure_cli('owner', 'api-url', 'hub-url'); \ + print(ops.list_services())" +``` + +## Management Operations + +### List Resources + +```python +from scripts.aenv_operations import AEnvOperations + +ops = AEnvOperations() +ops.configure_cli(owner_name, api_service_url, envhub_url) + +# List environments +envs = ops.list_environments() + +# List instances +instances = ops.list_instances() + +# List services +services = ops.list_services() +``` + +### Delete Resources + +```python +# Delete instance +ops.delete_instance(instance_id) + +# Delete service (keep storage) +ops.delete_service(service_id) + +# Delete service and storage +ops.delete_service(service_id, delete_storage=True) +``` + +## Reference Documentation + +- **[CLI_COMMANDS.md](references/CLI_COMMANDS.md)**: Quick reference for aenv CLI commands +- **[CONFIG_SCHEMA.md](references/CONFIG_SCHEMA.md)**: config.json structure and fields +- **[TROUBLESHOOTING.md](references/TROUBLESHOOTING.md)**: Common issues and solutions + +## Examples + +### Deploy Agent Instance + +```bash +python scripts/deploy_existing_env.py \ + --env-spec stockagent@1.0.2 \ + --owner-name trader-team \ + --api-service-url https://api.aenv.example.com \ + --envhub-url https://hub.aenv.example.com \ + --deploy-type instance \ + --ttl 100h \ + --env-vars '{ + "NEWS_API_KEY":"xxx", + "OPENAI_MODEL":"gpt-4", + "OPENAI_API_KEY":"xxx" + }' +``` + +### Deploy Web Service with Storage + +```bash +python scripts/deploy_existing_env.py \ + --env-spec webapp@2.0.0 \ + --owner-name dev-team \ + --api-service-url https://api.aenv.example.com \ + --envhub-url https://hub.aenv.example.com \ + --deploy-type service \ + --replicas 1 \ + --port 8081 \ + --enable-storage \ + --env-vars '{"DB_HOST":"postgres.svc"}' +``` + +### Build and Deploy New Environment + +```bash +python scripts/deploy_with_local_build.py \ + --env-name newagent \ + --owner-name my-team \ + --api-service-url https://api.aenv.example.com \ + --envhub-url https://hub.aenv.example.com \ + --registry-host registry.example.com \ + --registry-username user \ + --registry-password pass \ + --registry-namespace myteam \ + --deploy-type instance \ + --platform linux/amd64 +``` + +## Error Handling + +All scripts include automatic retry logic (2 retries by default). Common errors: + +- **Environment not found**: Verify environment is registered with `aenv list` +- **Registry auth failed**: Check registry credentials +- **Storage with multiple replicas**: Set `--replicas 1` when using `--enable-storage` +- **Build timeout**: Optimize Dockerfile, use smaller base images + +See [TROUBLESHOOTING.md](references/TROUBLESHOOTING.md) for detailed solutions. + +## Best Practices + +1. **Use semantic versioning** for environments (1.0.0, 1.0.1, 1.1.0, 2.0.0) +2. **Configure resources appropriately** in config.json (CPU, memory) +3. **Use Workflow C for repeated deployments** (most efficient) +4. **Set appropriate TTL for instances** (longer for long-running agents) +5. **Enable storage only when needed** (limits replicas to 1) +6. **Use environment variables** for configuration instead of hardcoding +7. **Delete unused resources** to free cluster capacity + +## Core Library + +The `scripts/aenv_operations.py` module provides the core operations used by all workflows. You can import and use it directly for custom workflows: + +```python +from scripts.aenv_operations import AEnvOperations, AEnvError + +try: + ops = AEnvOperations(verbose=True) + + # Configure CLI + ops.configure_cli( + owner_name="my-team", + api_service_url="https://api.example.com", + envhub_url="https://hub.example.com" + ) + + # Create instance + result = ops.create_instance( + env_spec="myenv@1.0.0", + ttl="24h", + env_vars={"KEY": "value"} + ) + + print(f"Instance created: {result['instance_id']}") + print(f"Access at: {result['ip_address']}") + +except AEnvError as e: + print(f"Deployment failed: {e}") +``` diff --git a/.claude/aenvironment-deploy/references/CLI_COMMANDS.md b/.claude/aenvironment-deploy/references/CLI_COMMANDS.md new file mode 100644 index 0000000..1baa373 --- /dev/null +++ b/.claude/aenvironment-deploy/references/CLI_COMMANDS.md @@ -0,0 +1,136 @@ +# AEnvironment CLI Commands Reference + +Quick reference for common aenv CLI commands. + +## Configuration + +```bash +# Set owner +aenv config set owner + +# Set storage type (local or aenv_hub) +aenv config set storage_config.type local + +# Set system URL +aenv config set system_url + +# Set EnvHub URL +aenv config set hub_config.hub_backend + +# Set registry configuration +aenv config set build_config.registry.host +aenv config set build_config.registry.username +aenv config set build_config.registry.password +aenv config set build_config.registry.namespace + +# Show current configuration +aenv config show +``` + +## Environment Management + +```bash +# Initialize new environment +aenv init + +# Initialize config-only (for existing Dockerfile) +aenv init --config-only + +# Build Docker image +aenv build --platform linux/amd64 --push + +# Register environment to EnvHub +aenv push + +# List registered environments +aenv list + +# Get environment details +aenv get +``` + +## Instance Operations + +```bash +# Create instance +aenv instance create @ \ + --ttl \ + --keep-alive \ + --skip-health \ + -e KEY=VALUE + +# List instances +aenv instance list + +# Get instance details +aenv instance get + +# Delete instance +aenv instance delete +``` + +## Service Operations + +```bash +# Create service +aenv service create @ \ + --replicas \ + --port \ + --enable-storage \ + -e KEY=VALUE + +# List services +aenv service list + +# Get service details +aenv service get + +# Update service +aenv service update --replicas +aenv service update -e KEY=VALUE + +# Delete service +aenv service delete +aenv service delete --delete-storage +``` + +## Common Patterns + +### Deploy Agent Instance + +```bash +aenv instance create myagent@1.0.0 \ + --ttl 100h \ + --keep-alive \ + --skip-health \ + -e OPENAI_API_KEY="xxx" \ + -e OPENAI_MODEL="gpt-4" +``` + +### Deploy Web Service + +```bash +aenv service create webapp@1.0.0 \ + --replicas 2 \ + --port 8080 \ + -e DATABASE_URL="postgres://..." +``` + +### Deploy Service with Storage + +```bash +aenv service create dataapp@1.0.0 \ + --replicas 1 \ + --port 8081 \ + --enable-storage +``` + +## Environment Specification Format + +Format: `@` + +Examples: + +- `myagent@1.0.0` +- `webapp@2.1.3` +- `stockagent@1.0.2` diff --git a/.claude/aenvironment-deploy/references/CONFIG_SCHEMA.md b/.claude/aenvironment-deploy/references/CONFIG_SCHEMA.md new file mode 100644 index 0000000..4372f37 --- /dev/null +++ b/.claude/aenvironment-deploy/references/CONFIG_SCHEMA.md @@ -0,0 +1,139 @@ +# Config.json Schema Reference + +Environment configuration file structure and key fields. + +## Basic Structure + +```json +{ + "name": "myenv", + "version": "1.0.0", + "description": "Environment description", + "artifacts": [ + { + "type": "image", + "content": "registry.example.com/myimage:latest" + } + ], + "requirements": { + "cpu": "1000m", + "memory": "2Gi" + }, + "deployConfig": { + "service": { + "replicas": 1, + "port": 8080, + "enableStorage": false, + "storageName": "myenv", + "storageSize": "10Gi", + "mountPath": "/home/admin/data" + } + } +} +``` + +## Key Fields + +### Basic Information + +- **name**: Environment name (required) +- **version**: Semantic version (required) +- **description**: Human-readable description + +### Artifacts + +Defines the Docker image to use: + +```json +"artifacts": [ + { + "type": "image", + "content": "registry.example.com/namespace/image:tag" + } +] +``` + +### Requirements + +Resource requirements: + +```json +"requirements": { + "cpu": "1000m", // CPU request/limit (e.g., "500m", "2") + "memory": "2Gi" // Memory request/limit (e.g., "512Mi", "4Gi") +} +``` + +Common values: + +- CPU: "100m", "500m", "1", "2" +- Memory: "128Mi", "512Mi", "1Gi", "2Gi", "4Gi" + +### Deploy Configuration (Service) + +Service-specific deployment settings: + +```json +"deployConfig": { + "service": { + "replicas": 1, // Number of replicas (1 if storage enabled) + "port": 8080, // Service port + "enableStorage": false, // Enable persistent storage + "storageName": "myenv", // PVC name (default: env name) + "storageSize": "10Gi", // Storage size + "mountPath": "/data" // Mount path in container + } +} +``` + +## Complete Example + +```json +{ + "name": "stockagent", + "version": "1.0.2", + "description": "Stock trading agent environment", + "artifacts": [ + { + "type": "image", + "content": "registry.example.com/agents/stockagent:1.0.2" + } + ], + "requirements": { + "cpu": "2", + "memory": "4Gi" + }, + "deployConfig": { + "service": { + "replicas": 1, + "port": 8080, + "enableStorage": true, + "storageName": "stockagent-data", + "storageSize": "20Gi", + "mountPath": "/home/admin/data" + } + } +} +``` + +## Storage Configuration Notes + +- **ReadWriteOnce**: When `enableStorage: true`, `replicas` must be 1 +- **storageName**: Defaults to environment name if not specified +- **storageSize**: Cannot be reduced after creation +- **mountPath**: Path where storage will be mounted in container + +## Version Guidelines + +Use semantic versioning: + +- MAJOR: Incompatible API changes +- MINOR: Backward-compatible functionality additions +- PATCH: Backward-compatible bug fixes + +Examples: + +- `1.0.0`: Initial release +- `1.0.1`: Bug fix +- `1.1.0`: New feature +- `2.0.0`: Breaking change diff --git a/.claude/aenvironment-deploy/references/TROUBLESHOOTING.md b/.claude/aenvironment-deploy/references/TROUBLESHOOTING.md new file mode 100644 index 0000000..a201b0a --- /dev/null +++ b/.claude/aenvironment-deploy/references/TROUBLESHOOTING.md @@ -0,0 +1,319 @@ +# Troubleshooting Guide + +Common issues and solutions when using AEnvironment. + +## Installation Issues + +### aenv command not found + +**Problem**: `aenv: command not found` after installation + +**Solution**: + +```bash +# Reinstall with --upgrade +pip install aenvironment --upgrade + +# Verify installation +aenv version + +# If still not found, check PATH +which aenv +python -m aenv version +``` + +### Permission denied + +**Problem**: Permission errors when running aenv + +**Solution**: + +```bash +# Use --user flag +pip install --user aenvironment --upgrade + +# Or use a virtual environment +python -m venv venv +source venv/bin/activate +pip install aenvironment +``` + +## Configuration Issues + +### Invalid configuration + +**Problem**: Configuration validation errors + +**Solution**: + +```bash +# Show current config +aenv config show + +# Reset specific setting +aenv config set owner + +# Verify each setting one by one +``` + +### Registry authentication failed + +**Problem**: Cannot push to Docker registry + +**Solution**: + +1. Verify registry credentials are correct +2. Check Docker is installed: `docker --version` +3. Test Docker login manually: + + ```bash + docker login -u -p + ``` + +4. Re-configure aenv registry settings + +## Build Issues + +### Docker build failed + +**Problem**: `aenv build` fails + +**Solutions**: + +**Check Dockerfile syntax**: + +```bash +cd +docker build -t test . +``` + +**Check platform compatibility**: + +```bash +# Use correct platform +aenv build --platform linux/amd64 +``` + +**Check Docker daemon**: + +```bash +docker ps # Should not error +``` + +### Build timeout + +**Problem**: Build takes too long and times out + +**Solution**: + +- Optimize Dockerfile (use smaller base images, multi-stage builds) +- Increase timeout in aenv_operations.py +- Build without push first: `docker build .` + +## Environment Registration Issues + +### Push failed: config.json not found + +**Problem**: `aenv push` fails with config.json error + +**Solution**: + +```bash +# Ensure you're in the right directory +ls config.json + +# Reinitialize if needed +aenv init --config-only +``` + +### Environment version conflict + +**Problem**: Version already exists in EnvHub + +**Solution**: + +1. Update version in config.json +2. Re-run `aenv push` + +```json +{ + "version": "1.0.1" // Increment version +} +``` + +## Deployment Issues + +### Instance creation failed + +**Problem**: `aenv instance create` fails + +**Common Causes**: + +1. **Environment not registered**: + + ```bash + # Check if environment exists + aenv list + aenv get + ``` + +2. **Invalid environment spec**: + + ```bash + # Use correct format: name@version + aenv instance create myenv@1.0.0 + ``` + +3. **Resource quota exceeded**: + - Check cluster resource limits + - Reduce CPU/memory in config.json + +### Instance not accessible + +**Problem**: Cannot access instance via IP + +**Solutions**: + +1. **Check instance status**: + + ```bash + aenv instance list + aenv instance get + ``` + +2. **Verify network access**: + + ```bash + ping + curl http://: + ``` + +3. **Check application logs** (if accessible): + - Application may not be listening on expected port + - Application may have crashed + +### Service creation failed with storage + +**Problem**: Service with storage fails to create + +**Solution**: + +```bash +# Ensure replicas=1 when storage enabled +aenv service create myapp@1.0.0 \ + --replicas 1 \ + --enable-storage \ + --port 8080 +``` + +**Check config.json**: + +```json +{ + "deployConfig": { + "service": { + "replicas": 1, // Must be 1 + "enableStorage": true + } + } +} +``` + +### Service URL not accessible + +**Problem**: Cannot access service via domain + +**Solutions**: + +1. **Check service status**: + + ```bash + aenv service list + aenv service get + ``` + +2. **Verify URL format**: + + ```text + http://.aenv-sandbox.svc.tydd-staging.alipay.net: + ``` + +3. **Check network access**: + - Ensure you're on the correct network (e.g., office network) + - Try from different network if possible + +## Common Error Messages + +### "Environment not found" + +**Cause**: Environment not registered in EnvHub + +**Solution**: + +```bash +# List all environments +aenv list + +# Register environment +cd +aenv push +``` + +### "ReadWriteOnce volume conflict" + +**Cause**: Multiple replicas with storage enabled + +**Solution**: +Set `replicas: 1` in config.json or command line + +### "Image pull failed" + +**Cause**: Registry authentication or image not found + +**Solutions**: + +1. Verify image name in config.json artifacts +2. Check registry credentials +3. Ensure image was pushed successfully + +### "Resource quota exceeded" + +**Cause**: Cluster resource limits reached + +**Solutions**: + +1. Reduce CPU/memory in config.json +2. Delete unused instances/services +3. Contact cluster administrator + +## Getting Help + +If issues persist: + +1. **Check configuration**: + + ```bash + aenv config show + ``` + +2. **Enable verbose output**: + + ```bash + # Use --verbose flag in workflow scripts + python scripts/deploy_with_local_build.py --verbose ... + ``` + +3. **Review recent deployments**: + + ```bash + aenv instance list + aenv service list + ``` + +4. **Check environment details**: + + ```bash + aenv get + aenv instance get + aenv service get + ``` diff --git a/.claude/aenvironment-deploy/scripts/aenv_operations.py b/.claude/aenvironment-deploy/scripts/aenv_operations.py new file mode 100755 index 0000000..5f34c9f --- /dev/null +++ b/.claude/aenvironment-deploy/scripts/aenv_operations.py @@ -0,0 +1,470 @@ +#!/usr/bin/env python3 +""" +AEnvironment Operations Library + +Core operations for interacting with AEnvironment CLI. +Used by high-level workflow scripts. +""" + +import json +import subprocess +import sys +from pathlib import Path +from typing import Dict, List, Optional, Tuple + + +class AEnvError(Exception): + """Base exception for AEnv operations.""" + pass + + +class AEnvOperations: + """Core AEnvironment CLI operations.""" + + def __init__(self, verbose: bool = False): + self.verbose = verbose + self._check_aenv_installed() + + def _check_aenv_installed(self) -> None: + """Check if aenv CLI is installed.""" + try: + result = subprocess.run( + ["aenv", "--help"], + capture_output=True, + text=True, + timeout=5 + ) + if result.returncode != 0: + raise AEnvError("aenv CLI is not properly installed") + except (subprocess.TimeoutExpired, FileNotFoundError) as e: + raise AEnvError(f"aenv CLI is not available: {e}") + + def _run_command( + self, + cmd: List[str], + cwd: Optional[str] = None, + timeout: int = 300, + retry: int = 2 + ) -> Tuple[int, str, str]: + """ + Run command with retry logic. + + Returns: (exit_code, stdout, stderr) + """ + for attempt in range(retry + 1): + try: + if self.verbose: + print(f"Running: {' '.join(cmd)}", file=sys.stderr) + + result = subprocess.run( + cmd, + capture_output=True, + text=True, + timeout=timeout, + cwd=cwd + ) + + if result.returncode == 0 or attempt == retry: + return result.returncode, result.stdout, result.stderr + + if self.verbose: + print(f"Attempt {attempt + 1} failed, retrying...", file=sys.stderr) + + except subprocess.TimeoutExpired: + if attempt == retry: + return 1, "", f"Command timed out after {timeout}s" + except Exception as e: + if attempt == retry: + return 1, "", str(e) + + return 1, "", "All retry attempts failed" + + def configure_cli( + self, + owner_name: str, + api_service_url: str, + envhub_url: str, + storage_type: str = "local", + registry_config: Optional[Dict[str, str]] = None + ) -> Dict: + """Configure AEnvironment CLI.""" + commands = [ + ["aenv", "config", "set", "owner", owner_name], + ["aenv", "config", "set", "storage_config.type", storage_type], + ["aenv", "config", "set", "system_url", api_service_url], + ["aenv", "config", "set", "hub_config.hub_backend", envhub_url], + ] + + if registry_config: + for key in ["host", "username", "password", "namespace"]: + if registry_config.get(key): + commands.append([ + "aenv", "config", "set", + f"build_config.registry.{key}", + registry_config[key] + ]) + + for cmd in commands: + exit_code, stdout, stderr = self._run_command(cmd) + if exit_code != 0: + raise AEnvError(f"Configuration failed: {stderr}") + + return {"status": "success", "message": "CLI configured"} + + def init_environment( + self, + env_name: str, + config_only: bool = False, + target_dir: Optional[str] = None + ) -> Dict: + """Initialize environment configuration.""" + work_dir = target_dir or "." + Path(work_dir).mkdir(parents=True, exist_ok=True) + + cmd = ["aenv", "init", env_name] + if config_only: + cmd.append("--config-only") + + exit_code, stdout, stderr = self._run_command(cmd, cwd=work_dir) + + if exit_code != 0: + raise AEnvError(f"Init failed: {stderr}") + + env_path = Path(work_dir) / env_name + return { + "status": "success", + "env_directory": str(env_path), + "message": f"Environment '{env_name}' initialized" + } + + def update_config_with_image( + self, + env_directory: str, + image_name: str + ) -> None: + """Update config.json with existing image.""" + config_path = Path(env_directory) / "config.json" + + if not config_path.exists(): + raise AEnvError(f"config.json not found in {env_directory}") + + with open(config_path, "r") as f: + config = json.load(f) + + config["artifacts"] = [{"type": "image", "content": image_name}] + + with open(config_path, "w") as f: + json.dump(config, f, indent=2) + + def build_image( + self, + env_directory: str, + platform: str = "linux/amd64", + push: bool = True + ) -> Dict: + """Build Docker image.""" + if not Path(env_directory).exists(): + raise AEnvError(f"Directory not found: {env_directory}") + + cmd = ["aenv", "build", "--platform", platform] + if push: + cmd.append("--push") + + exit_code, stdout, stderr = self._run_command( + cmd, + cwd=env_directory, + timeout=600 + ) + + if exit_code != 0: + raise AEnvError(f"Build failed: {stderr}") + + return { + "status": "success", + "message": "Image built successfully", + "output": stdout + } + + def register_environment(self, env_directory: str) -> Dict: + """Register environment to EnvHub.""" + if not Path(env_directory).exists(): + raise AEnvError(f"Directory not found: {env_directory}") + + cmd = ["aenv", "push"] + exit_code, stdout, stderr = self._run_command( + cmd, + cwd=env_directory, + timeout=300 + ) + + if exit_code != 0: + raise AEnvError(f"Registration failed: {stderr}") + + # Extract env info from config.json + config_path = Path(env_directory) / "config.json" + if config_path.exists(): + with open(config_path, "r") as f: + config = json.load(f) + env_name = config.get("name", "unknown") + version = config.get("version", "unknown") + else: + env_name = version = "unknown" + + return { + "status": "success", + "env_name": env_name, + "version": version, + "message": f"Environment {env_name}@{version} registered" + } + + def list_environments(self) -> List[str]: + """List registered environments.""" + cmd = ["aenv", "list"] + exit_code, stdout, stderr = self._run_command(cmd) + + if exit_code != 0: + raise AEnvError(f"List failed: {stderr}") + + # Parse environment names from rich table output + envs = [] + in_table = False + for line in stdout.split("\n"): + line = line.strip() + # Skip empty lines, headers, and table decorations + if not line or line.startswith("Available") or line.startswith("┏") or \ + line.startswith("┃") or line.startswith("┡") or line.startswith("└"): + continue + # Data rows start with │ + if line.startswith("│"): + parts = [p.strip() for p in line.split("│") if p.strip()] + if len(parts) >= 2: # name and version + env_name = parts[0] + version = parts[1] + envs.append(f"{env_name}@{version}") + + return envs + + def get_environment(self, env_name: str) -> Dict: + """Get environment details.""" + cmd = ["aenv", "get", env_name] + exit_code, stdout, stderr = self._run_command(cmd) + + if exit_code != 0: + raise AEnvError(f"Environment {env_name} not found: {stderr}") + + return {"status": "success", "output": stdout} + + def create_instance( + self, + env_spec: str, + ttl: str = "24h", + keep_alive: bool = True, + skip_health: bool = True, + env_vars: Optional[Dict[str, str]] = None + ) -> Dict: + """Create environment instance.""" + cmd = ["aenv", "instance", "create", env_spec, "--ttl", ttl] + + if keep_alive: + cmd.append("--keep-alive") + if skip_health: + cmd.append("--skip-health") + + if env_vars: + for key, value in env_vars.items(): + cmd.extend(["-e", f"{key}={value}"]) + + exit_code, stdout, stderr = self._run_command(cmd, timeout=180) + + if exit_code != 0: + raise AEnvError(f"Instance creation failed: {stderr}") + + # Extract instance info from output + instance_id = "unknown" + ip_address = "unknown" + + for line in stdout.split("\n"): + if "id" in line.lower(): + parts = line.split() + if len(parts) >= 2: + instance_id = parts[-1] + elif "ip" in line.lower() or "address" in line.lower(): + parts = line.split() + for part in parts: + if self._is_valid_ip(part): + ip_address = part + + return { + "status": "success", + "instance_id": instance_id, + "ip_address": ip_address, + "message": f"Instance created: {instance_id}", + "output": stdout + } + + def create_service( + self, + env_spec: str, + replicas: int = 1, + port: int = 8080, + enable_storage: bool = False, + env_vars: Optional[Dict[str, str]] = None + ) -> Dict: + """Create environment service.""" + if enable_storage and replicas > 1: + raise AEnvError("Storage requires replicas=1 (ReadWriteOnce)") + + cmd = [ + "aenv", "service", "create", env_spec, + "--replicas", str(replicas), + "--port", str(port) + ] + + if enable_storage: + cmd.append("--enable-storage") + + if env_vars: + for key, value in env_vars.items(): + cmd.extend(["-e", f"{key}={value}"]) + + exit_code, stdout, stderr = self._run_command(cmd, timeout=180) + + if exit_code != 0: + raise AEnvError(f"Service creation failed: {stderr}") + + # Extract service info from log output and table + service_id = "unknown" + access_url = "unknown" + + for line in stdout.split("\n"): + # Look for service ID in log line or table + if "service created:" in line.lower(): + parts = line.split(":") + if len(parts) >= 2: + service_id = parts[-1].strip().rstrip('[0m') + elif "│ service id" in line.lower(): + parts = [p.strip() for p in line.split("│") if p.strip()] + if len(parts) >= 2: + service_id = parts[1] + elif "│ service url" in line.lower(): + parts = [p.strip() for p in line.split("│") if p.strip()] + if len(parts) >= 2: + access_url = parts[1] + + return { + "status": "success", + "service_id": service_id, + "access_url": access_url, + "message": f"Service created: {service_id}", + "output": stdout + } + + def list_instances(self) -> str: + """List instances.""" + cmd = ["aenv", "instance", "list"] + exit_code, stdout, stderr = self._run_command(cmd) + + if exit_code != 0: + raise AEnvError(f"List instances failed: {stderr}") + + return stdout + + def get_instance_details(self, instance_id: str) -> Dict: + """Get instance details.""" + cmd = ["aenv", "instance", "get", instance_id] + exit_code, stdout, stderr = self._run_command(cmd) + + if exit_code != 0: + raise AEnvError(f"Get instance failed: {stderr}") + + return {"status": "success", "output": stdout} + + def list_services(self) -> str: + """List services.""" + cmd = ["aenv", "service", "list"] + exit_code, stdout, stderr = self._run_command(cmd) + + if exit_code != 0: + raise AEnvError(f"List services failed: {stderr}") + + return stdout + + def get_service_details(self, service_id: str) -> Dict: + """Get service details.""" + cmd = ["aenv", "service", "get", service_id] + exit_code, stdout, stderr = self._run_command(cmd) + + if exit_code != 0: + raise AEnvError(f"Get service failed: {stderr}") + + return {"status": "success", "output": stdout} + + def delete_instance(self, instance_id: str) -> Dict: + """Delete instance.""" + cmd = ["aenv", "instance", "delete", instance_id, "-y"] + exit_code, stdout, stderr = self._run_command(cmd) + + if exit_code != 0: + raise AEnvError(f"Delete instance failed: {stderr}") + + return { + "status": "success", + "message": f"Instance {instance_id} deleted" + } + + def delete_service( + self, + service_id: str, + delete_storage: bool = False + ) -> Dict: + """Delete service.""" + cmd = ["aenv", "service", "delete", service_id, "-y"] + if delete_storage: + cmd.append("--delete-storage") + + exit_code, stdout, stderr = self._run_command(cmd) + + if exit_code != 0: + raise AEnvError(f"Delete service failed: {stderr}") + + return { + "status": "success", + "message": f"Service {service_id} deleted" + } + + def update_service( + self, + service_id: str, + replicas: Optional[int] = None, + environment_variables: Optional[Dict[str, str]] = None + ) -> Dict: + """Update service configuration.""" + cmd = ["aenv", "service", "update", service_id] + + if replicas is not None: + cmd.extend(["--replicas", str(replicas)]) + + if environment_variables: + for key, value in environment_variables.items(): + cmd.extend(["-e", f"{key}={value}"]) + + exit_code, stdout, stderr = self._run_command(cmd) + + if exit_code != 0: + raise AEnvError(f"Update service failed: {stderr}") + + return { + "status": "success", + "message": f"Service {service_id} updated" + } + + @staticmethod + def _is_valid_ip(ip_str: str) -> bool: + """Check if string is valid IP address.""" + parts = ip_str.split(".") + if len(parts) != 4: + return False + try: + return all(0 <= int(part) <= 255 for part in parts) + except ValueError: + return False diff --git a/.claude/aenvironment-deploy/scripts/deploy_existing_env.py b/.claude/aenvironment-deploy/scripts/deploy_existing_env.py new file mode 100755 index 0000000..efffa62 --- /dev/null +++ b/.claude/aenvironment-deploy/scripts/deploy_existing_env.py @@ -0,0 +1,126 @@ +#!/usr/bin/env python3 +""" +Workflow C: Deploy Existing Environment + +This workflow handles deployment of already registered environments: +1. Configure CLI +2. Verify environment exists +3. Deploy as instance or service +""" + +import argparse +import json +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).parent)) +from aenv_operations import AEnvOperations, AEnvError + + +def main(): + parser = argparse.ArgumentParser( + description="Deploy existing registered environment" + ) + + # Required parameters + parser.add_argument("--env-spec", required=True, + help="Environment spec (name@version)") + parser.add_argument("--owner-name", required=True, + help="Owner name") + parser.add_argument("--api-service-url", required=True, + help="API service URL") + parser.add_argument("--envhub-url", required=True, + help="EnvHub URL") + + # Deployment type + parser.add_argument("--deploy-type", choices=["instance", "service"], + required=True, help="Deployment type") + + # Instance-specific options + parser.add_argument("--ttl", default="24h", + help="Instance TTL (default: 24h)") + parser.add_argument("--env-vars", type=json.loads, default={}, + help='Environment variables as JSON') + + # Service-specific options + parser.add_argument("--replicas", type=int, default=1, + help="Service replicas (default: 1)") + parser.add_argument("--port", type=int, default=8080, + help="Service port (default: 8080)") + parser.add_argument("--enable-storage", action="store_true", + help="Enable persistent storage for service") + + # Other options + parser.add_argument("--verbose", action="store_true", + help="Verbose output") + + args = parser.parse_args() + + try: + ops = AEnvOperations(verbose=args.verbose) + + print("Step 1/3: Configuring CLI...") + ops.configure_cli( + owner_name=args.owner_name, + api_service_url=args.api_service_url, + envhub_url=args.envhub_url, + storage_type="local" + ) + print("✓ CLI configured") + + print(f"\nStep 2/3: Verifying environment '{args.env_spec}'...") + # Extract env name from spec (before @) + env_name = args.env_spec.split('@')[0] + + try: + result = ops.get_environment(env_name) + print(f"✓ Environment exists") + if args.verbose: + print(f"\n{result['output']}") + except AEnvError: + print(f"\n⚠ Warning: Could not verify environment") + print(f" Continuing with deployment attempt...") + + print(f"\nStep 3/3: Deploying {args.deploy_type}...") + if args.deploy_type == "instance": + result = ops.create_instance( + env_spec=args.env_spec, + ttl=args.ttl, + keep_alive=True, + skip_health=True, + env_vars=args.env_vars + ) + print(f"✓ Instance created: {result['instance_id']}") + print(f" IP Address: {result['ip_address']}") + if result.get('output'): + print(f"\n{result['output']}") + + else: # service + result = ops.create_service( + env_spec=args.env_spec, + replicas=args.replicas, + port=args.port, + enable_storage=args.enable_storage, + env_vars=args.env_vars + ) + print(f"✓ Service created: {result['service_id']}") + print(f" Access URL: {result['access_url']}") + if result.get('output'): + print(f"\n{result['output']}") + + print("\n✅ Deployment completed successfully!") + return 0 + + except AEnvError as e: + print(f"\n❌ Error: {e}", file=sys.stderr) + return 1 + except KeyboardInterrupt: + print("\n\n⚠ Deployment cancelled by user", file=sys.stderr) + return 130 + except Exception as e: + print(f"\n❌ Unexpected error: {e}", file=sys.stderr) + return 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/.claude/aenvironment-deploy/scripts/deploy_with_existing_image.py b/.claude/aenvironment-deploy/scripts/deploy_with_existing_image.py new file mode 100755 index 0000000..5c3a12d --- /dev/null +++ b/.claude/aenvironment-deploy/scripts/deploy_with_existing_image.py @@ -0,0 +1,145 @@ +#!/usr/bin/env python3 +""" +Workflow B: Deploy with Existing Image + +This workflow handles: +1. Configure CLI +2. Initialize environment (config-only mode) +3. Update config.json with existing image +4. Register environment to EnvHub +5. Deploy as instance or service +""" + +import argparse +import json +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).parent)) +from aenv_operations import AEnvOperations, AEnvError + + +def main(): + parser = argparse.ArgumentParser( + description="Deploy with existing Docker image" + ) + + # Required parameters + parser.add_argument("--env-name", required=True, + help="Environment name") + parser.add_argument("--image-name", required=True, + help="Existing Docker image name") + parser.add_argument("--owner-name", required=True, + help="Owner name") + parser.add_argument("--api-service-url", required=True, + help="API service URL") + parser.add_argument("--envhub-url", required=True, + help="EnvHub URL") + + # Deployment type + parser.add_argument("--deploy-type", choices=["instance", "service"], + required=True, help="Deployment type") + + # Instance-specific options + parser.add_argument("--ttl", default="24h", + help="Instance TTL (default: 24h)") + parser.add_argument("--env-vars", type=json.loads, default={}, + help='Environment variables as JSON') + + # Service-specific options + parser.add_argument("--replicas", type=int, default=1, + help="Service replicas (default: 1)") + parser.add_argument("--port", type=int, default=8080, + help="Service port (default: 8080)") + parser.add_argument("--enable-storage", action="store_true", + help="Enable persistent storage for service") + + # Other options + parser.add_argument("--work-dir", default=".", + help="Working directory (default: current)") + parser.add_argument("--verbose", action="store_true", + help="Verbose output") + + args = parser.parse_args() + + try: + ops = AEnvOperations(verbose=args.verbose) + + print("Step 1/5: Configuring CLI...") + ops.configure_cli( + owner_name=args.owner_name, + api_service_url=args.api_service_url, + envhub_url=args.envhub_url, + storage_type="local" + ) + print("✓ CLI configured") + + print(f"\nStep 2/5: Initializing environment config '{args.env_name}'...") + result = ops.init_environment( + env_name=args.env_name, + config_only=True, + target_dir=args.work_dir + ) + env_dir = result["env_directory"] + print(f"✓ Environment config initialized at {env_dir}") + + print(f"\nStep 3/5: Updating config with image '{args.image_name}'...") + ops.update_config_with_image(env_dir, args.image_name) + print("✓ Config updated with image") + + print("\n⚠ IMPORTANT: Please review the config.json file and customize:") + print(f" - {env_dir}/config.json") + print(" - CPU/memory requirements") + print(" - Version number") + print(" - Storage configuration (if needed)") + print("\nPress Enter when ready to continue...") + input() + + print(f"\nStep 4/5: Registering environment to EnvHub...") + result = ops.register_environment(env_dir) + env_spec = f"{result['env_name']}@{result['version']}" + print(f"✓ Environment registered: {env_spec}") + + print(f"\nStep 5/5: Deploying {args.deploy_type}...") + if args.deploy_type == "instance": + result = ops.create_instance( + env_spec=env_spec, + ttl=args.ttl, + keep_alive=True, + skip_health=True, + env_vars=args.env_vars + ) + print(f"✓ Instance created: {result['instance_id']}") + print(f" IP Address: {result['ip_address']}") + if result.get('output'): + print(f"\n{result['output']}") + + else: # service + result = ops.create_service( + env_spec=env_spec, + replicas=args.replicas, + port=args.port, + enable_storage=args.enable_storage, + env_vars=args.env_vars + ) + print(f"✓ Service created: {result['service_id']}") + print(f" Access URL: {result['access_url']}") + if result.get('output'): + print(f"\n{result['output']}") + + print("\n✅ Deployment completed successfully!") + return 0 + + except AEnvError as e: + print(f"\n❌ Error: {e}", file=sys.stderr) + return 1 + except KeyboardInterrupt: + print("\n\n⚠ Deployment cancelled by user", file=sys.stderr) + return 130 + except Exception as e: + print(f"\n❌ Unexpected error: {e}", file=sys.stderr) + return 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/.claude/aenvironment-deploy/scripts/deploy_with_local_build.py b/.claude/aenvironment-deploy/scripts/deploy_with_local_build.py new file mode 100755 index 0000000..2c95dcc --- /dev/null +++ b/.claude/aenvironment-deploy/scripts/deploy_with_local_build.py @@ -0,0 +1,168 @@ +#!/usr/bin/env python3 +""" +Workflow A: Deploy with Local Image Build + +This workflow handles the complete process of: +1. Configure CLI +2. Initialize environment +3. Build Docker image locally +4. Register environment to EnvHub +5. Deploy as instance or service +""" + +import argparse +import json +import sys +from pathlib import Path + +# Add scripts directory to path +sys.path.insert(0, str(Path(__file__).parent)) +from aenv_operations import AEnvOperations, AEnvError + + +def main(): + parser = argparse.ArgumentParser( + description="Deploy with local image build" + ) + + # Required parameters + parser.add_argument("--env-name", required=True, + help="Environment name") + parser.add_argument("--owner-name", required=True, + help="Owner name") + parser.add_argument("--api-service-url", required=True, + help="API service URL") + parser.add_argument("--envhub-url", required=True, + help="EnvHub URL") + + # Registry configuration (required for local build) + parser.add_argument("--registry-host", required=True, + help="Registry host") + parser.add_argument("--registry-username", required=True, + help="Registry username") + parser.add_argument("--registry-password", required=True, + help="Registry password") + parser.add_argument("--registry-namespace", required=True, + help="Registry namespace") + + # Deployment type + parser.add_argument("--deploy-type", choices=["instance", "service"], + required=True, help="Deployment type") + + # Instance-specific options + parser.add_argument("--ttl", default="24h", + help="Instance TTL (default: 24h)") + parser.add_argument("--env-vars", type=json.loads, default={}, + help='Environment variables as JSON (e.g., \'{"KEY":"VALUE"}\')') + + # Service-specific options + parser.add_argument("--replicas", type=int, default=1, + help="Service replicas (default: 1)") + parser.add_argument("--port", type=int, default=8080, + help="Service port (default: 8080)") + parser.add_argument("--enable-storage", action="store_true", + help="Enable persistent storage for service") + + # Other options + parser.add_argument("--work-dir", default=".", + help="Working directory (default: current)") + parser.add_argument("--platform", default="linux/amd64", + help="Build platform (default: linux/amd64)") + parser.add_argument("--verbose", action="store_true", + help="Verbose output") + + args = parser.parse_args() + + try: + ops = AEnvOperations(verbose=args.verbose) + + print("Step 1/5: Configuring CLI...") + registry_config = { + "host": args.registry_host, + "username": args.registry_username, + "password": args.registry_password, + "namespace": args.registry_namespace + } + + ops.configure_cli( + owner_name=args.owner_name, + api_service_url=args.api_service_url, + envhub_url=args.envhub_url, + storage_type="local", + registry_config=registry_config + ) + print("✓ CLI configured") + + print(f"\nStep 2/5: Initializing environment '{args.env_name}'...") + result = ops.init_environment( + env_name=args.env_name, + config_only=False, + target_dir=args.work_dir + ) + env_dir = result["env_directory"] + print(f"✓ Environment initialized at {env_dir}") + + print("\n⚠ IMPORTANT: Please edit the config.json file in the environment") + print(f" directory ({env_dir}) to customize:") + print(" - CPU/memory requirements") + print(" - Version number") + print(" - Storage configuration (if needed)") + print("\nPress Enter when ready to continue...") + input() + + print(f"\nStep 3/5: Building Docker image...") + ops.build_image( + env_directory=env_dir, + platform=args.platform, + push=True + ) + print("✓ Image built and pushed") + + print(f"\nStep 4/5: Registering environment to EnvHub...") + result = ops.register_environment(env_dir) + env_spec = f"{result['env_name']}@{result['version']}" + print(f"✓ Environment registered: {env_spec}") + + print(f"\nStep 5/5: Deploying {args.deploy_type}...") + if args.deploy_type == "instance": + result = ops.create_instance( + env_spec=env_spec, + ttl=args.ttl, + keep_alive=True, + skip_health=True, + env_vars=args.env_vars + ) + print(f"✓ Instance created: {result['instance_id']}") + print(f" IP Address: {result['ip_address']}") + if result.get('output'): + print(f"\n{result['output']}") + + else: # service + result = ops.create_service( + env_spec=env_spec, + replicas=args.replicas, + port=args.port, + enable_storage=args.enable_storage, + env_vars=args.env_vars + ) + print(f"✓ Service created: {result['service_id']}") + print(f" Access URL: {result['access_url']}") + if result.get('output'): + print(f"\n{result['output']}") + + print("\n✅ Deployment completed successfully!") + return 0 + + except AEnvError as e: + print(f"\n❌ Error: {e}", file=sys.stderr) + return 1 + except KeyboardInterrupt: + print("\n\n⚠ Deployment cancelled by user", file=sys.stderr) + return 130 + except Exception as e: + print(f"\n❌ Unexpected error: {e}", file=sys.stderr) + return 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/README.md b/README.md index 0556a9e..8570595 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,7 @@ ## 📰 News +- **Deploy Skill** (Feb 2026) - 🎉 New **Claude Code Skill** for automated deployment! Deploy instances and services directly from Claude Code with support for three workflows: local build, existing image, and registered environments. [Get Started](#deploy-skill) - **v0.1.4** (Jan 2026) - AEnv CLI now supports **instance** and **service** management! Deploy and manage your agents and applications with simple commands. See [CLI Guide](./docs/guide/cli.md) for details. --- @@ -131,6 +132,52 @@ AEnvironment comes with several built-in environments ready to use: 📖 For detailed setup instructions, see the [Quick Start Guide](./docs/getting_started/quickstart.md). +### Deploy Skill + +The easiest way to deploy AEnvironment instances and services is using our Claude Code Skill. This skill provides automated deployment workflows with full support for instance and service management. + +#### Install Deploy Skill + +```bash +# Install from GitHub releases +curl -L https://github.com/inclusionAI/AEnvironment/releases/latest/download/aenvironment-deploy.skill -o aenvironment-deploy.skill +claude skill install aenvironment-deploy.skill +``` + +#### Use Deploy Skill + +Once installed, you can deploy directly from Claude Code: + +**Deploy an existing environment:** + +```python +# Simply ask Claude Code: +# "Deploy game-2048@1.0.6 as an instance with 1 hour TTL" +# "Deploy myapp@2.0.0 as a service with storage enabled" +``` + +**Supported workflows:** + +- **Workflow A**: Build Docker image locally and deploy +- **Workflow B**: Register existing Docker image and deploy +- **Workflow C**: Deploy already registered environments (simplest) + +**Deployment types:** + +- **Instance**: Temporary environment with IP access (for agents, testing) +- **Service**: Persistent service with domain access and optional storage (for production apps) + +The skill automatically handles: + +- ✅ CLI configuration and validation +- ✅ Environment registration +- ✅ Instance/service creation +- ✅ Environment variable injection +- ✅ Resource management (list, update, delete) +- ✅ Error handling and retry + +📖 See the [Deploy Skill Guide](./.claude/aenvironment-deploy/SKILL.md) for detailed documentation. + ### Install SDK and init Environment ```bash