Host-Side API Server — Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Build a PoC of two host-side API servers (Go container builder, Python repo provisioner) callable from inside an OpenShell sandbox, managed by a language-agnostic Go orchestrator.
Architecture: Go orchestrator starts both servers via a uniform process contract (start → healthcheck → register → SIGTERM), provisions an OpenShell sandbox with L7 policy allowing egress to the servers, and passes endpoint info + bearer token into the sandbox. The agent inside calls the servers via curl through the L7 proxy at 10.200.0.1:3128.
Tech Stack: Go 1.23 (orchestrator + builder server), Python 3.11+ (repo provisioner), OpenShell (sandbox + L7 proxy), shell scripts (setup/run).
Spec: experiments/host-side-api-server/superpowers/2026-05-14-host-side-api-server-design.md
Reference experiment: experiments/agent-scoped-tools-triage/ — reuse sandbox lifecycle patterns from launcher/sandbox.py and L7 policy format from policies/triage-write.yaml.
All paths below are relative to experiments/host-side-api-server/.
Task 1: Go module scaffolding and container builder server
Files:
Create:
servers/builder/main.goCreate:
servers/builder/go.mod[ ] Step 1: Initialize Go module
cd experiments/host-side-api-server
mkdir -p servers/builder
cd servers/builder
go mod init github.com/fullsend-ai/experiments/host-side-api-server/servers/builder- [ ] Step 2: Write the builder server
Create servers/builder/main.go:
package main
import (
"context"
"encoding/json"
"flag"
"fmt"
"log"
"net/http"
"os"
"os/exec"
"os/signal"
"strings"
"sync"
"syscall"
"time"
"github.com/google/uuid"
)
type BuildRequest struct {
Dockerfile string `json:"dockerfile"`
ContextDir string `json:"context_dir"`
Tag string `json:"tag"`
}
type BuildResponse struct {
ID string `json:"id"`
Tag string `json:"tag"`
Status string `json:"status"`
Output string `json:"output,omitempty"`
Error string `json:"error,omitempty"`
}
type PushRequest struct {
Tag string `json:"tag"`
}
type Server struct {
token string
mu sync.Mutex
builds map[string]*BuildResponse
registry string
}
func (s *Server) authMiddleware(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
auth := r.Header.Get("Authorization")
if auth != "Bearer "+s.token {
http.Error(w, `{"error":"unauthorized"}`, http.StatusUnauthorized)
return
}
next(w, r)
}
}
func (s *Server) handleHealthz(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(`{"status":"ok"}`))
}
func (s *Server) handleBuild(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, `{"error":"method not allowed"}`, http.StatusMethodNotAllowed)
return
}
var req BuildRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, `{"error":"invalid request body"}`, http.StatusBadRequest)
return
}
if req.Tag == "" {
http.Error(w, `{"error":"tag is required"}`, http.StatusBadRequest)
return
}
buildID := uuid.New().String()
resp := &BuildResponse{ID: buildID, Tag: req.Tag, Status: "building"}
s.mu.Lock()
s.builds[buildID] = resp
s.mu.Unlock()
// Run build synchronously — this tests long-running operations
dockerfile := req.Dockerfile
if dockerfile == "" {
dockerfile = "Dockerfile"
}
contextDir := req.ContextDir
if contextDir == "" {
contextDir = "."
}
// Try podman first, fall back to docker
builder := "podman"
if _, err := exec.LookPath("podman"); err != nil {
builder = "docker"
}
cmd := exec.Command(builder, "build", "-t", req.Tag, "-f", dockerfile, contextDir)
output, err := cmd.CombinedOutput()
s.mu.Lock()
if err != nil {
resp.Status = "failed"
resp.Error = err.Error()
resp.Output = string(output)
} else {
resp.Status = "completed"
resp.Output = string(output)
}
s.mu.Unlock()
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(resp)
}
func (s *Server) handlePush(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, `{"error":"method not allowed"}`, http.StatusMethodNotAllowed)
return
}
var req PushRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, `{"error":"invalid request body"}`, http.StatusBadRequest)
return
}
if req.Tag == "" {
http.Error(w, `{"error":"tag is required"}`, http.StatusBadRequest)
return
}
builder := "podman"
if _, err := exec.LookPath("podman"); err != nil {
builder = "docker"
}
cmd := exec.Command(builder, "push", req.Tag)
output, err := cmd.CombinedOutput()
resp := map[string]string{"tag": req.Tag}
if err != nil {
resp["status"] = "failed"
resp["error"] = err.Error()
resp["output"] = string(output)
} else {
resp["status"] = "pushed"
resp["output"] = string(output)
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(resp)
}
func (s *Server) handleImages(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, `{"error":"method not allowed"}`, http.StatusMethodNotAllowed)
return
}
builder := "podman"
if _, err := exec.LookPath("podman"); err != nil {
builder = "docker"
}
cmd := exec.Command(builder, "images", "--format", "json")
output, err := cmd.CombinedOutput()
if err != nil {
http.Error(w, fmt.Sprintf(`{"error":"%s"}`, err.Error()), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
w.Write(output)
}
func (s *Server) handleOpenAPI(w http.ResponseWriter, r *http.Request) {
spec := map[string]interface{}{
"openapi": "3.0.0",
"info": map[string]string{"title": "Container Builder API", "version": "0.1.0"},
"paths": map[string]interface{}{
"/build": map[string]interface{}{
"post": map[string]interface{}{
"summary": "Build a container image",
"description": "Builds a container image from a Dockerfile and context directory using podman or docker on the host.",
"requestBody": map[string]interface{}{
"required": true,
"content": map[string]interface{}{
"application/json": map[string]interface{}{
"schema": map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"dockerfile": map[string]string{"type": "string", "description": "Path to Dockerfile (default: Dockerfile)"},
"context_dir": map[string]string{"type": "string", "description": "Build context directory (default: .)"},
"tag": map[string]string{"type": "string", "description": "Image tag (required)"},
},
"required": []string{"tag"},
},
},
},
},
},
},
"/push": map[string]interface{}{
"post": map[string]interface{}{
"summary": "Push a built image to a registry",
"requestBody": map[string]interface{}{
"required": true,
"content": map[string]interface{}{
"application/json": map[string]interface{}{
"schema": map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"tag": map[string]string{"type": "string", "description": "Image tag to push (required)"},
},
"required": []string{"tag"},
},
},
},
},
},
},
"/images": map[string]interface{}{
"get": map[string]interface{}{
"summary": "List locally built images",
},
},
},
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(spec)
}
func (s *Server) handleToolsJSON(w http.ResponseWriter, r *http.Request) {
tools := map[string]interface{}{
"tools": []map[string]interface{}{
{
"name": "build_container_image",
"description": "Build a container image from a Dockerfile and context directory using podman or docker on the host.",
"endpoint": "POST /build",
"input_schema": map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"dockerfile": map[string]string{"type": "string", "description": "Path to Dockerfile (default: Dockerfile)"},
"context_dir": map[string]string{"type": "string", "description": "Build context directory (default: .)"},
"tag": map[string]string{"type": "string", "description": "Image tag (required)"},
},
"required": []string{"tag"},
},
},
{
"name": "push_image",
"description": "Push a built container image to a registry.",
"endpoint": "POST /push",
"input_schema": map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"tag": map[string]string{"type": "string", "description": "Image tag to push (required)"},
},
"required": []string{"tag"},
},
},
{
"name": "list_images",
"description": "List all locally built container images.",
"endpoint": "GET /images",
"input_schema": map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{},
},
},
},
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(tools)
}
func main() {
port := flag.Int("port", 9090, "Port to listen on")
token := flag.String("token", "", "Bearer token for authentication")
flag.Parse()
if *token == "" {
log.Fatal("--token is required")
}
s := &Server{
token: *token,
builds: make(map[string]*BuildResponse),
}
mux := http.NewServeMux()
mux.HandleFunc("/healthz", s.handleHealthz)
mux.HandleFunc("/build", s.authMiddleware(s.handleBuild))
mux.HandleFunc("/push", s.authMiddleware(s.handlePush))
mux.HandleFunc("/images", s.authMiddleware(s.handleImages))
mux.HandleFunc("/openapi.json", s.handleOpenAPI)
mux.HandleFunc("/tools.json", s.handleToolsJSON)
srv := &http.Server{
Addr: fmt.Sprintf(":%d", *port),
Handler: mux,
}
go func() {
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, syscall.SIGTERM, syscall.SIGINT)
<-sigCh
log.Println("Shutting down...")
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
srv.Shutdown(ctx)
}()
log.Printf("Builder server listening on :%d", *port)
if err := srv.ListenAndServe(); err != http.ErrServerClosed {
log.Fatal(err)
}
}- [ ] Step 3: Add uuid dependency
cd servers/builder
go get github.com/google/uuid- [ ] Step 4: Build and verify it compiles
cd servers/builder
go build -o builder-server .Expected: binary builder-server created with no errors.
- [ ] Step 5: Smoke test the server locally
cd servers/builder
TOKEN=$(uuidgen)
./builder-server --port 9090 --token "$TOKEN" &
SERVER_PID=$!
sleep 1
# Health check (no auth required)
curl -s http://localhost:9090/healthz
# Expected: {"status":"ok"}
# Auth check — no token
curl -s -o /dev/null -w "%{http_code}" http://localhost:9090/images
# Expected: 401
# Auth check — valid token
curl -s -H "Authorization: Bearer $TOKEN" http://localhost:9090/images
# Expected: JSON array of images (may be empty)
# OpenAPI spec
curl -s http://localhost:9090/openapi.json | python3 -m json.tool | head -5
# Expected: valid JSON with openapi field
# Tools schema
curl -s http://localhost:9090/tools.json | python3 -m json.tool | head -5
# Expected: valid JSON with tools array
kill $SERVER_PID- [ ] Step 6: Clean up binary and commit
rm servers/builder/builder-server
cd experiments/host-side-api-server
git add servers/builder/
git commit -m "feat: add Go container builder API server
Serves /build, /push, /images endpoints with bearer token auth.
Includes /openapi.json and /tools.json for discoverability testing."Task 2: Python repo provisioner server
Files:
Create:
servers/repo-provisioner/server.pyCreate:
servers/repo-provisioner/requirements.txt[ ] Step 1: Create requirements.txt
Create servers/repo-provisioner/requirements.txt:
# No external dependencies — stdlib only (http.server, subprocess, json)- [ ] Step 2: Write the repo provisioner server
Create servers/repo-provisioner/server.py:
#!/usr/bin/env python3
"""
REST API server that clones repos, scans for malicious content,
and copies clean repos into an OpenShell sandbox via SCP.
Usage:
python3 server.py --port 9091 --token <uuid> [--config config.json]
The --config file is optional and may contain:
{"github_token": "ghp_...", "sandbox_name": "sandbox-xxx", "ssh_config": "/path/to/ssh-config"}
"""
import argparse
import json
import os
import re
import shutil
import subprocess
import sys
import tempfile
import threading
import uuid as uuid_mod
from http.server import BaseHTTPRequestHandler, HTTPServer
from pathlib import Path
from signal import SIGTERM, signal
INJECTION_PATTERNS = [
re.compile(r"ignore\s+(all\s+)?(previous|prior|above)\s+instructions", re.IGNORECASE),
re.compile(r"you\s+are\s+now\s+in\s+(\w+\s+)?mode", re.IGNORECASE),
re.compile(r"system\s*:\s*you\s+are", re.IGNORECASE),
re.compile(r"<\s*system\s*>", re.IGNORECASE),
re.compile(r"IMPORTANT:\s*ignore", re.IGNORECASE),
re.compile(r"act\s+as\s+(a|an)\s+", re.IGNORECASE),
re.compile(r"disregard\s+(all\s+)?(previous|prior)", re.IGNORECASE),
re.compile(r"new\s+instructions?\s*:", re.IGNORECASE),
]
TEXT_EXTENSIONS = {".md", ".txt", ".rst", ".adoc", ".html", ".htm", ".json", ".yaml", ".yml", ".toml"}
def scan_repo(repo_path: str) -> list[dict]:
"""Scan a cloned repo for security issues. Returns list of findings."""
findings = []
repo = Path(repo_path)
# Check for executable git hooks
hooks_dir = repo / ".git" / "hooks"
if hooks_dir.exists():
for hook in hooks_dir.iterdir():
if hook.is_file() and os.access(hook, os.X_OK):
if not hook.name.endswith(".sample"):
findings.append({
"type": "executable_hook",
"path": str(hook.relative_to(repo)),
"severity": "high",
"description": f"Executable git hook: {hook.name}",
})
# Check for symlinks pointing outside the repo
for path in repo.rglob("*"):
if path.is_symlink():
target = path.resolve()
try:
target.relative_to(repo.resolve())
except ValueError:
findings.append({
"type": "symlink_escape",
"path": str(path.relative_to(repo)),
"target": str(target),
"severity": "high",
"description": f"Symlink points outside repo: {path.name} -> {target}",
})
# Check for prompt injection patterns in text files
for path in repo.rglob("*"):
if path.is_file() and path.suffix.lower() in TEXT_EXTENSIONS:
try:
content = path.read_text(errors="ignore")
for pattern in INJECTION_PATTERNS:
match = pattern.search(content)
if match:
findings.append({
"type": "prompt_injection",
"path": str(path.relative_to(repo)),
"severity": "medium",
"description": f"Potential prompt injection: '{match.group()}'",
})
break
except (OSError, UnicodeDecodeError):
pass
# Check for suspicious binary files in unexpected locations
suspicious_dirs = {".github", ".gitlab", ".vscode", ".idea"}
for sdir in suspicious_dirs:
dirpath = repo / sdir
if dirpath.exists():
for path in dirpath.rglob("*"):
if path.is_file() and not path.suffix.lower() in TEXT_EXTENSIONS | {".png", ".jpg", ".svg", ".gif", ".ico"}:
try:
with open(path, "rb") as f:
header = f.read(4)
if header[:2] == b"\x7fE" or header[:4] == b"\xcf\xfa\xed\xfe":
findings.append({
"type": "suspicious_binary",
"path": str(path.relative_to(repo)),
"severity": "medium",
"description": f"Binary executable in config directory: {path.name}",
})
except OSError:
pass
return findings
def clone_repo(repo: str, ref: str, dest: str, github_token: str | None = None) -> subprocess.CompletedProcess:
"""Clone a repo to dest. Uses github_token if provided."""
url = f"https://github.com/{repo}.git"
env = {**os.environ}
if github_token:
url = f"https://x-access-token:{github_token}@github.com/{repo}.git"
return subprocess.run(
["git", "clone", "--depth", "1", "--branch", ref, url, dest],
capture_output=True,
text=True,
env=env,
timeout=120,
)
def copy_to_sandbox(local_path: str, ssh_config: str, sandbox_name: str, remote_path: str) -> subprocess.CompletedProcess:
"""Copy a directory into a sandbox via SCP."""
return subprocess.run(
[
"scp", "-F", ssh_config, "-r",
local_path,
f"openshell-{sandbox_name}:{remote_path}",
],
capture_output=True,
text=True,
timeout=120,
)
def make_handler(token: str, config: dict) -> type:
"""Create HTTP handler with token and config bound."""
github_token = config.get("github_token")
sandbox_name = config.get("sandbox_name", "")
ssh_config = config.get("ssh_config", "")
jobs: dict[str, dict] = {}
jobs_lock = threading.Lock()
class RepoProvisionerHandler(BaseHTTPRequestHandler):
def do_GET(self):
if self.path == "/healthz":
self._send_json(200, {"status": "ok"})
return
# GET /repo/status/{id}
if self.path.startswith("/repo/status/"):
self._check_auth()
job_id = self.path.split("/")[-1]
with jobs_lock:
job = jobs.get(job_id)
if job is None:
self._send_json(404, {"error": "job not found"})
else:
self._send_json(200, job)
return
if self.path == "/openapi.json":
self._send_openapi()
return
if self.path == "/tools.json":
self._send_tools()
return
self._send_json(404, {"error": "not found"})
def do_POST(self):
if self.path == "/repo/provision":
if not self._check_auth():
return
content_length = int(self.headers.get("Content-Length", 0))
body = json.loads(self.rfile.read(content_length))
repo = body.get("repo", "")
ref = body.get("ref", "main")
if not repo or "/" not in repo:
self._send_json(400, {"error": "repo must be in org/name format"})
return
job_id = str(uuid_mod.uuid4())
job = {"id": job_id, "repo": repo, "ref": ref, "status": "cloning"}
with jobs_lock:
jobs[job_id] = job
# Run synchronously for PoC
with tempfile.TemporaryDirectory() as tmpdir:
clone_dest = os.path.join(tmpdir, repo.split("/")[-1])
# Clone
result = clone_repo(repo, ref, clone_dest, github_token)
if result.returncode != 0:
job["status"] = "failed"
job["error"] = f"Clone failed: {result.stderr}"
self._send_json(500, job)
return
# Scan
job["status"] = "scanning"
findings = scan_repo(clone_dest)
high_findings = [f for f in findings if f["severity"] == "high"]
if high_findings:
job["status"] = "rejected"
job["findings"] = findings
self._send_json(200, job)
return
# Copy to sandbox (if configured)
if sandbox_name and ssh_config:
job["status"] = "copying"
cp_result = copy_to_sandbox(
clone_dest, ssh_config, sandbox_name, "/tmp/workspace/"
)
if cp_result.returncode != 0:
job["status"] = "failed"
job["error"] = f"SCP failed: {cp_result.stderr}"
self._send_json(500, job)
return
job["status"] = "completed"
job["findings"] = findings
self._send_json(200, job)
return
self._send_json(404, {"error": "not found"})
def _check_auth(self) -> bool:
auth = self.headers.get("Authorization", "")
if auth != f"Bearer {token}":
self._send_json(401, {"error": "unauthorized"})
return False
return True
def _send_json(self, status: int, data: dict):
self.send_response(status)
self.send_header("Content-Type", "application/json")
self.end_headers()
self.wfile.write(json.dumps(data).encode())
def _send_openapi(self):
spec = {
"openapi": "3.0.0",
"info": {"title": "Secure Repo Provisioner API", "version": "0.1.0"},
"paths": {
"/repo/provision": {
"post": {
"summary": "Clone, scan, and provision a repo into the sandbox",
"description": "Clones a GitHub repo, scans for security issues (executable hooks, symlink escapes, prompt injection, suspicious binaries), and copies the clean repo into the sandbox. Rejects repos with high-severity findings.",
"requestBody": {
"required": True,
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"repo": {"type": "string", "description": "Repository in org/name format"},
"ref": {"type": "string", "description": "Git ref to clone (default: main)"},
},
"required": ["repo"],
}
}
},
},
}
},
"/repo/status/{id}": {
"get": {
"summary": "Check provisioning status",
"description": "Returns the current status of a provisioning job.",
}
},
},
}
self._send_json(200, spec)
def _send_tools(self):
tools = {
"tools": [
{
"name": "provision_repo",
"description": "Clone a GitHub repo, scan it for security issues, and copy it into the sandbox if scans pass. Rejects repos with executable git hooks, symlink escapes, prompt injection patterns, or suspicious binaries.",
"endpoint": "POST /repo/provision",
"input_schema": {
"type": "object",
"properties": {
"repo": {"type": "string", "description": "Repository in org/name format (e.g. 'org/repo')"},
"ref": {"type": "string", "description": "Git ref to clone (default: main)"},
},
"required": ["repo"],
},
},
{
"name": "check_provision_status",
"description": "Check the status of a repo provisioning job.",
"endpoint": "GET /repo/status/{id}",
"input_schema": {
"type": "object",
"properties": {
"id": {"type": "string", "description": "Job ID returned by provision_repo"},
},
"required": ["id"],
},
},
]
}
self._send_json(200, tools)
def log_message(self, format, *args):
print(f"[repo-provisioner] {args[0]} {args[1]} {args[2]}", file=sys.stderr)
return RepoProvisionerHandler
def main():
parser = argparse.ArgumentParser(description="Secure Repo Provisioner API")
parser.add_argument("--port", type=int, default=9091)
parser.add_argument("--token", required=True)
parser.add_argument("--config", help="Path to JSON config file")
args = parser.parse_args()
config = {}
if args.config:
with open(args.config) as f:
config = json.load(f)
handler = make_handler(args.token, config)
server = HTTPServer(("", args.port), handler)
def shutdown_handler(signum, frame):
print("[repo-provisioner] Shutting down...", file=sys.stderr)
threading.Thread(target=server.shutdown).start()
signal(SIGTERM, shutdown_handler)
print(f"[repo-provisioner] Listening on :{args.port}", file=sys.stderr)
server.serve_forever()
if __name__ == "__main__":
main()- [ ] Step 3: Smoke test the server locally
cd experiments/host-side-api-server
TOKEN=$(uuidgen)
python3 servers/repo-provisioner/server.py --port 9091 --token "$TOKEN" &
SERVER_PID=$!
sleep 1
# Health check
curl -s http://localhost:9091/healthz
# Expected: {"status": "ok"}
# Auth check — no token
curl -s -o /dev/null -w "%{http_code}" -X POST http://localhost:9091/repo/provision
# Expected: 401
# Provision a public repo (no sandbox configured, so no SCP step)
curl -s -H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"repo": "octocat/Hello-World", "ref": "master"}' \
http://localhost:9091/repo/provision | python3 -m json.tool
# Expected: JSON with status "completed" and findings array
# OpenAPI spec
curl -s http://localhost:9091/openapi.json | python3 -m json.tool | head -5
# Tools schema
curl -s http://localhost:9091/tools.json | python3 -m json.tool | head -5
kill $SERVER_PID- [ ] Step 4: Commit
git add servers/repo-provisioner/
git commit -m "feat: add Python secure repo provisioner API server
Clones repos, scans for executable hooks, symlink escapes, prompt
injection patterns, and suspicious binaries. Copies into sandbox
via SCP only if scans pass. Includes /openapi.json and /tools.json."Task 3: Go orchestrator
Files:
Create:
orchestrator/main.goCreate:
orchestrator/go.mod[ ] Step 1: Initialize Go module
mkdir -p experiments/host-side-api-server/orchestrator
cd experiments/host-side-api-server/orchestrator
go mod init github.com/fullsend-ai/experiments/host-side-api-server/orchestrator- [ ] Step 2: Write the orchestrator
Create orchestrator/main.go:
package main
import (
"encoding/json"
"flag"
"fmt"
"log"
"net/http"
"os"
"os/exec"
"os/signal"
"path/filepath"
"strings"
"syscall"
"time"
"github.com/google/uuid"
)
type ServerConfig struct {
Name string `json:"name"`
Command string `json:"command"`
Port int `json:"port"`
}
type RunningServer struct {
Config ServerConfig
Process *os.Process
}
func resolveHostIP() (string, error) {
cmd := exec.Command("getent", "hosts", "host.openshell.internal")
out, err := cmd.Output()
if err == nil {
parts := strings.Fields(string(out))
if len(parts) > 0 {
return parts[0], nil
}
}
// Fallback: try to resolve via docker bridge
cmd = exec.Command("ip", "route", "show", "default")
out, err = cmd.Output()
if err != nil {
return "", fmt.Errorf("cannot resolve host IP: %w", err)
}
parts := strings.Fields(string(out))
for i, p := range parts {
if p == "via" && i+1 < len(parts) {
return parts[i+1], nil
}
}
return "", fmt.Errorf("cannot determine host IP from route table")
}
func startServer(cfg ServerConfig, token string) (*RunningServer, error) {
args := strings.Fields(cfg.Command)
args = append(args, "--port", fmt.Sprintf("%d", cfg.Port), "--token", token)
cmd := exec.Command(args[0], args[1:]...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Start(); err != nil {
return nil, fmt.Errorf("failed to start %s: %w", cfg.Name, err)
}
// Poll healthz
url := fmt.Sprintf("http://localhost:%d/healthz", cfg.Port)
for i := 0; i < 30; i++ {
resp, err := http.Get(url)
if err == nil && resp.StatusCode == 200 {
resp.Body.Close()
log.Printf("[orchestrator] %s is ready on :%d", cfg.Name, cfg.Port)
return &RunningServer{Config: cfg, Process: cmd.Process}, nil
}
time.Sleep(500 * time.Millisecond)
}
cmd.Process.Kill()
return nil, fmt.Errorf("%s did not become healthy within 15s", cfg.Name)
}
func stopServer(srv *RunningServer) {
log.Printf("[orchestrator] Stopping %s (pid %d)", srv.Config.Name, srv.Process.Pid)
srv.Process.Signal(syscall.SIGTERM)
done := make(chan struct{})
go func() {
srv.Process.Wait()
close(done)
}()
select {
case <-done:
log.Printf("[orchestrator] %s stopped", srv.Config.Name)
case <-time.After(5 * time.Second):
log.Printf("[orchestrator] %s did not stop, killing", srv.Config.Name)
srv.Process.Kill()
}
}
func renderPolicy(templatePath string, hostIP string) (string, error) {
content, err := os.ReadFile(templatePath)
if err != nil {
return "", err
}
rendered := strings.ReplaceAll(string(content), "{{HOST_IP}}", hostIP)
tmpFile, err := os.CreateTemp("", "policy-*.yaml")
if err != nil {
return "", err
}
tmpFile.WriteString(rendered)
tmpFile.Close()
return tmpFile.Name(), nil
}
func createSandbox(name string) error {
cmd := exec.Command("timeout", "60",
"openshell", "sandbox", "create",
"--name", name,
"--keep",
"--no-auto-providers",
"--no-tty",
)
cmd.Stdin = nil
cmd.Stderr = os.Stderr
err := cmd.Run()
// timeout exits 124 — check if sandbox exists
if err != nil {
check := exec.Command("openshell", "sandbox", "get", name)
if checkErr := check.Run(); checkErr != nil {
return fmt.Errorf("sandbox create failed: %w", err)
}
}
// Poll for ready
for i := 0; i < 30; i++ {
check := exec.Command("openshell", "sandbox", "get", name)
out, err := check.Output()
if err == nil && strings.Contains(string(out), "Ready") {
return nil
}
time.Sleep(2 * time.Second)
}
return fmt.Errorf("sandbox %s not ready after 60s", name)
}
func applyPolicy(sandboxName string, policyPath string) error {
for attempt := 1; attempt <= 3; attempt++ {
cmd := exec.Command("openshell", "policy", "set", sandboxName,
"--policy", policyPath, "--wait")
cmd.Stderr = os.Stderr
if err := cmd.Run(); err == nil {
return nil
}
log.Printf("[orchestrator] Policy attempt %d failed, retrying in 3s...", attempt)
time.Sleep(3 * time.Second)
}
return fmt.Errorf("policy set failed after 3 attempts")
}
func getSSHConfig(sandboxName string) (string, error) {
cmd := exec.Command("openshell", "sandbox", "ssh-config", sandboxName)
out, err := cmd.Output()
if err != nil {
return "", err
}
tmpFile, err := os.CreateTemp("", "ssh-config-*")
if err != nil {
return "", err
}
tmpFile.Write(out)
tmpFile.Close()
return tmpFile.Name(), nil
}
func sandboxSSH(sshConfigPath string, sandboxName string, command string, timeout int) (string, error) {
cmd := exec.Command("ssh", "-F", sshConfigPath, fmt.Sprintf("openshell-%s", sandboxName), command)
out, err := cmd.CombinedOutput()
return string(out), err
}
func deleteSandbox(name string) {
exec.Command("openshell", "sandbox", "delete", name).Run()
}
func main() {
policyPath := flag.String("policy", "", "Path to L7 policy template YAML")
serversFile := flag.String("servers", "", "Path to JSON file listing servers to start")
sandboxName := flag.String("sandbox", "api-server-test", "OpenShell sandbox name")
agentCmd := flag.String("agent-command", "", "Command to run inside the sandbox (e.g. curl test)")
flag.Parse()
if *policyPath == "" || *serversFile == "" {
log.Fatal("--policy and --servers are required")
}
// Generate per-run token
token := uuid.New().String()
log.Printf("[orchestrator] Per-run token: %s", token)
// Read server configs
serversData, err := os.ReadFile(*serversFile)
if err != nil {
log.Fatalf("Cannot read servers file: %v", err)
}
var serverConfigs []ServerConfig
if err := json.Unmarshal(serversData, &serverConfigs); err != nil {
log.Fatalf("Cannot parse servers file: %v", err)
}
// Start servers
var running []*RunningServer
defer func() {
for _, srv := range running {
stopServer(srv)
}
}()
for _, cfg := range serverConfigs {
srv, err := startServer(cfg, token)
if err != nil {
log.Fatalf("Failed to start server: %v", err)
}
running = append(running, srv)
}
// Resolve host IP and render policy
hostIP, err := resolveHostIP()
if err != nil {
log.Fatalf("Cannot resolve host IP: %v", err)
}
log.Printf("[orchestrator] Host IP: %s", hostIP)
renderedPolicy, err := renderPolicy(*policyPath, hostIP)
if err != nil {
log.Fatalf("Cannot render policy: %v", err)
}
defer os.Remove(renderedPolicy)
// Create sandbox
log.Printf("[orchestrator] Creating sandbox %s...", *sandboxName)
if err := createSandbox(*sandboxName); err != nil {
log.Fatalf("Sandbox creation failed: %v", err)
}
defer deleteSandbox(*sandboxName)
// Apply policy
log.Printf("[orchestrator] Applying L7 policy...")
if err := applyPolicy(*sandboxName, renderedPolicy); err != nil {
log.Fatalf("Policy application failed: %v", err)
}
// Get SSH config
sshConfigPath, err := getSSHConfig(*sandboxName)
if err != nil {
log.Fatalf("Cannot get SSH config: %v", err)
}
defer os.Remove(sshConfigPath)
// Export token and endpoint info into sandbox
builderURL := fmt.Sprintf("http://%s:9090", hostIP)
provisionerURL := fmt.Sprintf("http://%s:9091", hostIP)
envSetup := fmt.Sprintf(
"export API_TOKEN='%s' BUILDER_URL='%s' PROVISIONER_URL='%s'",
token, builderURL, provisionerURL,
)
sandboxSSH(sshConfigPath, *sandboxName,
fmt.Sprintf("echo \"%s\" >> ~/.bashrc", envSetup), 10)
// Run agent command if provided
if *agentCmd != "" {
log.Printf("[orchestrator] Running agent command in sandbox...")
absAgentCmd := *agentCmd
// Read the command file if it's a file path
if _, err := os.Stat(absAgentCmd); err == nil {
content, err := os.ReadFile(absAgentCmd)
if err != nil {
log.Fatalf("Cannot read agent command file: %v", err)
}
absAgentCmd = string(content)
}
// Prepend env setup since .bashrc isn't sourced for non-interactive ssh
fullCmd := envSetup + " && " + absAgentCmd
output, err := sandboxSSH(sshConfigPath, *sandboxName, fullCmd, 300)
fmt.Println(output)
if err != nil {
log.Printf("[orchestrator] Agent command failed: %v", err)
}
} else {
log.Printf("[orchestrator] No --agent-command provided. Sandbox ready for manual testing.")
log.Printf("[orchestrator] SSH config: %s", sshConfigPath)
log.Printf("[orchestrator] Token: %s", token)
log.Printf("[orchestrator] Builder: %s", builderURL)
log.Printf("[orchestrator] Provisioner: %s", provisionerURL)
// Wait for interrupt
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, syscall.SIGTERM, syscall.SIGINT)
log.Println("[orchestrator] Press Ctrl+C to shut down")
<-sigCh
}
log.Println("[orchestrator] Shutting down...")
// Extract results dir from sandbox
resultsDir := filepath.Join("results", time.Now().Format("2006-01-02-150405"))
os.MkdirAll(resultsDir, 0o755)
}- [ ] Step 3: Add uuid dependency
cd experiments/host-side-api-server/orchestrator
go get github.com/google/uuid- [ ] Step 4: Build and verify it compiles
cd experiments/host-side-api-server/orchestrator
go build -o orchestrator .Expected: binary orchestrator created with no errors.
- [ ] Step 5: Clean up and commit
rm experiments/host-side-api-server/orchestrator/orchestrator
git add experiments/host-side-api-server/orchestrator/
git commit -m "feat: add Go orchestrator for API server lifecycle
Language-agnostic process contract: reads server configs from JSON,
starts servers, polls healthz, provisions OpenShell sandbox with
L7 policy, injects token + endpoints, and cleans up on exit."Task 4: L7 policies
Files:
Create:
policies/full-access.yamlCreate:
policies/restricted.yaml[ ] Step 1: Write full-access policy
Create policies/full-access.yaml:
version: 1
# Baseline policy: all API server endpoints allowed.
# Both builder (:9090) and provisioner (:9091) reachable from sandbox.
filesystem_policy:
include_workdir: true
read_only: [/usr, /lib, /proc, /dev/urandom, /etc, /var/log]
read_write: [/tmp, /dev/null]
landlock:
compatibility: best_effort
process:
run_as_user: sandbox
run_as_group: sandbox
network_policies:
builder:
name: container-builder
endpoints:
- host: host.openshell.internal
port: 9090
protocol: rest
enforcement: enforce
rules:
- allow:
method: GET
path: /healthz
- allow:
method: POST
path: /build
- allow:
method: POST
path: /push
- allow:
method: GET
path: /images
- allow:
method: GET
path: /openapi.json
- allow:
method: GET
path: /tools.json
allowed_ips:
- "{{HOST_IP}}/32"
binaries:
- path: "**/curl"
provisioner:
name: repo-provisioner
endpoints:
- host: host.openshell.internal
port: 9091
protocol: rest
enforcement: enforce
rules:
- allow:
method: GET
path: /healthz
- allow:
method: POST
path: /repo/provision
- allow:
method: GET
path: /repo/status/*
- allow:
method: GET
path: /openapi.json
- allow:
method: GET
path: /tools.json
allowed_ips:
- "{{HOST_IP}}/32"
binaries:
- path: "**/curl"- [ ] Step 2: Write restricted policy
Create policies/restricted.yaml:
version: 1
# Restricted policy: only build and provision endpoints.
# No /push, no /images — tests agent handling of 403s.
filesystem_policy:
include_workdir: true
read_only: [/usr, /lib, /proc, /dev/urandom, /etc, /var/log]
read_write: [/tmp, /dev/null]
landlock:
compatibility: best_effort
process:
run_as_user: sandbox
run_as_group: sandbox
network_policies:
builder:
name: container-builder-restricted
endpoints:
- host: host.openshell.internal
port: 9090
protocol: rest
enforcement: enforce
rules:
- allow:
method: GET
path: /healthz
- allow:
method: POST
path: /build
- allow:
method: GET
path: /openapi.json
- allow:
method: GET
path: /tools.json
allowed_ips:
- "{{HOST_IP}}/32"
binaries:
- path: "**/curl"
provisioner:
name: repo-provisioner-restricted
endpoints:
- host: host.openshell.internal
port: 9091
protocol: rest
enforcement: enforce
rules:
- allow:
method: GET
path: /healthz
- allow:
method: POST
path: /repo/provision
- allow:
method: GET
path: /repo/status/*
- allow:
method: GET
path: /openapi.json
- allow:
method: GET
path: /tools.json
allowed_ips:
- "{{HOST_IP}}/32"
binaries:
- path: "**/curl"- [ ] Step 3: Commit
git add policies/
git commit -m "feat: add L7 network policies for API server access
Full-access baseline and restricted variant (no /push, no /images)
for testing agent behavior on 403 responses."Task 5: Agent definitions for discoverability comparison
Files:
Create:
agents/openapi-discovery.mdCreate:
agents/tooluse-discovery.mdCreate:
agents/baked-instructions.md[ ] Step 1: Write OpenAPI discovery agent
Create agents/openapi-discovery.md:
---
name: openapi-discovery-agent
description: Tests API discoverability via OpenAPI spec
---
# API Discovery Agent (OpenAPI)
You are inside an OpenShell sandbox. Two API servers are running on the host
and accessible via curl through the network proxy.
## How to discover available APIs
Fetch the OpenAPI spec from each server to learn what endpoints are available:
```bash
curl -s "$BUILDER_URL/openapi.json" | python3 -m json.tool
curl -s "$PROVISIONER_URL/openapi.json" | python3 -m json.toolAuthentication
All API requests require a bearer token. Use the $API_TOKEN environment variable:
curl -H "Authorization: Bearer $API_TOKEN" ...Your task
- Discover the available APIs by reading the OpenAPI specs
- Use the builder API to build a simple container image
- Use the provisioner API to provision a public repository
- Report what worked and what didn't
If any endpoint returns a 403, note it — the network policy may not allow that endpoint. This is expected behavior, not an error.
- [ ] **Step 2: Write tool-use schema discovery agent**
Create `agents/tooluse-discovery.md`:
```markdown
---
name: tooluse-discovery-agent
description: Tests API discoverability via tool-use schema
---
# API Discovery Agent (Tool-Use Schema)
You are inside an OpenShell sandbox. Two API servers are running on the host
and accessible via curl through the network proxy.
## How to discover available APIs
Fetch the tool definitions from each server:
```bash
curl -s "$BUILDER_URL/tools.json" | python3 -m json.tool
curl -s "$PROVISIONER_URL/tools.json" | python3 -m json.toolEach tool definition includes a name, description, endpoint (HTTP method and path), and input schema. Call the endpoints using curl with the appropriate HTTP method and JSON body.
Authentication
All API requests require a bearer token. Use the $API_TOKEN environment variable:
curl -H "Authorization: Bearer $API_TOKEN" ...Your task
- Discover the available tools by reading the tool schemas
- Use the builder tools to build a simple container image
- Use the provisioner tools to provision a public repository
- Report what worked and what didn't
If any endpoint returns a 403, note it — the network policy may not allow that endpoint. This is expected behavior, not an error.
- [ ] **Step 3: Write baked-instructions agent**
Create `agents/baked-instructions.md`:
```markdown
---
name: baked-instructions-agent
description: Tests API discoverability via hardcoded instructions
---
# API Agent (Baked Instructions)
You are inside an OpenShell sandbox. Two API servers are running on the host
and accessible via curl through the network proxy.
## Authentication
All API requests require a bearer token:
```bash
curl -H "Authorization: Bearer $API_TOKEN" ...Container Builder API ($BUILDER_URL)
POST /build
Build a container image from a Dockerfile.
curl -X POST -H "Authorization: Bearer $API_TOKEN" \
-H "Content-Type: application/json" \
-d '{"tag": "myimage:latest", "dockerfile": "Dockerfile", "context_dir": "."}' \
"$BUILDER_URL/build"Request body:
tag(required): Image tagdockerfile(optional, default "Dockerfile"): Path to Dockerfilecontext_dir(optional, default "."): Build context directory
POST /push
Push a built image to a registry.
curl -X POST -H "Authorization: Bearer $API_TOKEN" \
-H "Content-Type: application/json" \
-d '{"tag": "myimage:latest"}' \
"$BUILDER_URL/push"GET /images
List locally built images.
curl -H "Authorization: Bearer $API_TOKEN" "$BUILDER_URL/images"Secure Repo Provisioner API ($PROVISIONER_URL)
POST /repo/provision
Clone a repo, scan for security issues, and copy into the sandbox.
curl -X POST -H "Authorization: Bearer $API_TOKEN" \
-H "Content-Type: application/json" \
-d '{"repo": "org/name", "ref": "main"}' \
"$PROVISIONER_URL/repo/provision"Request body:
repo(required): Repository in org/name formatref(optional, default "main"): Git ref
GET /repo/status/{id}
Check provisioning status.
curl -H "Authorization: Bearer $API_TOKEN" "$PROVISIONER_URL/repo/status/<job-id>"Your task
- Use the builder API to build a simple container image
- Use the provisioner API to provision a public repository
- Report what worked and what didn't
If any endpoint returns a 403, note it — the network policy may not allow that endpoint. This is expected behavior, not an error.
- [ ] **Step 4: Commit**
```bash
git add agents/
git commit -m "feat: add three agent definitions for discoverability comparison
OpenAPI spec, tool-use schema, and baked-in instructions approaches
for testing how agents discover and use host-side APIs."Task 6: In-sandbox build test script
Files:
Create:
sandbox-build-test/test-in-sandbox-build.sh[ ] Step 1: Write the test script
Create sandbox-build-test/test-in-sandbox-build.sh:
#!/usr/bin/env bash
set -euo pipefail
# Test whether rootless podman/buildah works inside an OpenShell sandbox.
# Expected: it doesn't, due to seccomp blocking CLONE_NEWUSER, AF_NETLINK, setns.
# See: https://github.com/NVIDIA/OpenShell/issues/113
echo "=== Testing container builds inside OpenShell sandbox ==="
echo ""
# Create a minimal Dockerfile
cat > /tmp/test-dockerfile <<'DOCKERFILE'
FROM alpine:latest
RUN echo "hello from inside sandbox build"
DOCKERFILE
echo "--- Attempting: podman build ---"
if command -v podman &>/dev/null; then
podman build -t test-in-sandbox:latest -f /tmp/test-dockerfile /tmp 2>&1 || true
echo ""
echo "Exit code: $?"
else
echo "podman not found in PATH"
fi
echo ""
echo "--- Attempting: buildah bud ---"
if command -v buildah &>/dev/null; then
buildah bud -t test-in-sandbox:latest -f /tmp/test-dockerfile /tmp 2>&1 || true
echo ""
echo "Exit code: $?"
else
echo "buildah not found in PATH"
fi
echo ""
echo "--- Attempting: docker build ---"
if command -v docker &>/dev/null; then
docker build -t test-in-sandbox:latest -f /tmp/test-dockerfile /tmp 2>&1 || true
echo ""
echo "Exit code: $?"
else
echo "docker not found in PATH"
fi
echo ""
echo "=== In-sandbox build test complete ==="
echo "If all attempts failed, the sandbox limitation is confirmed."
echo "If any succeeded, document what changed since OpenShell#113."
rm -f /tmp/test-dockerfile- [ ] Step 2: Make executable and commit
chmod +x sandbox-build-test/test-in-sandbox-build.sh
git add sandbox-build-test/
git commit -m "feat: add in-sandbox container build test script
Tests whether podman/buildah/docker work inside OpenShell sandbox.
Expected to fail due to seccomp restrictions (NVIDIA/OpenShell#113)."Task 7: Server config, run script, and HOW_TO
Files:
Create:
servers.jsonCreate:
run.shCreate:
setup.shCreate:
HOW_TO.mdCreate:
README.md[ ] Step 1: Write servers.json
Create servers.json:
[
{
"name": "builder",
"command": "go run ./servers/builder",
"port": 9090
},
{
"name": "repo-provisioner",
"command": "python3 ./servers/repo-provisioner/server.py",
"port": 9091
}
]- [ ] Step 2: Write setup.sh
Create setup.sh:
#!/usr/bin/env bash
set -euo pipefail
echo "=== Host-Side API Server Experiment Setup ==="
# Check prerequisites
MISSING=""
for cmd in go python3 openshell curl git uuidgen; do
if ! command -v "$cmd" &>/dev/null; then
MISSING="$MISSING $cmd"
fi
done
if [ -n "$MISSING" ]; then
echo "ERROR: Missing required tools:$MISSING"
exit 1
fi
echo "All prerequisites found."
# Check openshell gateway
if ! openshell gateway info &>/dev/null 2>&1; then
echo "ERROR: OpenShell gateway not running. Start it with: openshell gateway start"
exit 1
fi
echo "OpenShell gateway is running."
# Build Go servers
echo "Building Go builder server..."
(cd servers/builder && go build -o ../../bin/builder-server .)
echo "Building Go orchestrator..."
(cd orchestrator && go build -o ../bin/orchestrator .)
mkdir -p results
echo ""
echo "Setup complete. Run ./run.sh to start the experiment."- [ ] Step 3: Write run.sh
Create run.sh:
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
cd "$SCRIPT_DIR"
POLICY="${1:-policies/full-access.yaml}"
AGENT_CMD="${2:-}"
SANDBOX_NAME="api-server-test-$(date +%s)"
echo "=== Host-Side API Server Experiment ==="
echo "Policy: $POLICY"
echo "Sandbox: $SANDBOX_NAME"
# Build if needed
if [ ! -f bin/orchestrator ] || [ ! -f bin/builder-server ]; then
echo "Running setup first..."
./setup.sh
fi
# Run orchestrator
if [ -n "$AGENT_CMD" ]; then
./bin/orchestrator \
--policy "$POLICY" \
--servers servers.json \
--sandbox "$SANDBOX_NAME" \
--agent-command "$AGENT_CMD"
else
echo "No agent command specified. Starting in interactive mode."
echo "The orchestrator will start servers and sandbox, then wait for Ctrl+C."
echo ""
./bin/orchestrator \
--policy "$POLICY" \
--servers servers.json \
--sandbox "$SANDBOX_NAME"
fi- [ ] Step 4: Write HOW_TO.md
Create HOW_TO.md:
## Purpose
Run the host-side API server experiment to validate that agents inside an
OpenShell sandbox can call API servers running on the host through the L7
proxy.
## Requirements
| Requirement | Link |
|-------------|------|
| Go 1.23+ | https://go.dev/dl/ |
| Python 3.11+ | https://www.python.org/downloads/ |
| OpenShell CLI | https://github.com/NVIDIA/OpenShell |
| Docker or Podman | https://docs.docker.com/get-docker/ |
| curl | (pre-installed on most systems) |
| git | https://git-scm.com/downloads |
### Environment variables
No environment variables are required for basic usage. For private repo
provisioning, create a `config.json` with a `github_token` field.
## Steps
1. Navigate to the experiment directory:
```bash
cd experiments/host-side-api-serverEnsure the OpenShell gateway is running:
bashopenshell gateway startRun the setup script to build Go binaries and verify prerequisites:
bash./setup.shRun the experiment with full-access policy (interactive mode):
bash./run.shThis starts both API servers, creates a sandbox, and waits. You can SSH into the sandbox and test manually.
Run with restricted policy to test 403 handling:
bash./run.sh policies/restricted.yamlRun the in-sandbox build test (from inside the sandbox):
bash./sandbox-build-test/test-in-sandbox-build.sh
Expected Output
- Both API servers start and pass health checks
- Sandbox is created with L7 policy applied
- From inside the sandbox,
curlto API server endpoints succeeds for allowed endpoints and returns 403 for restricted ones - In-sandbox build test fails (confirming OpenShell#113 limitation)
- Container build via host API completes successfully
- Repo provisioning clones, scans, and reports results
- [ ] **Step 5: Write README.md**
Create `README.md`:
```markdown
# Experiment: Host-Side API Server for Sandboxed Agents
Tracking issue: [fullsend-ai/experiments#25](https://github.com/fullsend-ai/experiments/issues/25)
## What this experiment covers
1. **Basic API server lifecycle** — two API servers started by the
orchestrator, callable from inside an OpenShell sandbox via the L7 proxy
2. **Credential isolation** — servers hold credentials internally, agents
never see them
3. **Container build delegation** — Go server builds images via podman/docker
on the host, working around OpenShell's seccomp restrictions
4. **API discoverability** — three approaches compared: OpenAPI spec,
tool-use schema, baked-in agent instructions
5. **Per-run auth** — UUID bearer token generated per run
6. **Long-running operations** — container builds that exceed MCP timeout
7. **L7 policy tuning** — most restrictive policy that allows the API
## Architecture
See [design spec](superpowers/2026-05-14-host-side-api-server-design.md).
## Quick start
See [HOW_TO.md](HOW_TO.md).
## Key design decisions
- **Two servers in different languages** (Go + Python) to validate the
language-agnostic process contract
- **Uniform process contract**: `--port`, `--token`, `/healthz`, SIGTERM
- **Repo provisioner depends on OpenShell#1272**: if content inspection hooks
ship in OpenShell, the scan-before-copy flow could be handled natively
## Findings
See [results/findings.md](results/findings.md) (populated after running).- [ ] Step 6: Make scripts executable and commit
chmod +x setup.sh run.sh
mkdir -p results
touch results/.gitkeep
git add servers.json run.sh setup.sh HOW_TO.md README.md results/.gitkeep
git commit -m "feat: add orchestration scripts, HOW_TO, and README
Includes servers.json config, setup.sh (build + prerequisite check),
run.sh (orchestrator launcher), HOW_TO.md (reproduction steps),
and README.md (experiment overview)."Task 8: End-to-end smoke test
This task verifies the full pipeline works locally before documenting results.
- [ ] Step 1: Run setup
cd experiments/host-side-api-server
./setup.shExpected: "Setup complete" with Go binaries in bin/.
- [ ] Step 2: Start in interactive mode with full-access policy
./run.sh policies/full-access.yamlExpected: Both servers start, sandbox is created, policy applied. Orchestrator prints SSH config path, token, and server URLs.
- [ ] Step 3: Test from inside sandbox (in another terminal)
Use the SSH config printed by the orchestrator:
ssh -F <ssh-config-path> openshell-<sandbox-name> \
"export API_TOKEN='<token>' BUILDER_URL='http://<host-ip>:9090' PROVISIONER_URL='http://<host-ip>:9091' && \
curl -s -H 'Authorization: Bearer \$API_TOKEN' \$BUILDER_URL/healthz && echo '' && \
curl -s -H 'Authorization: Bearer \$API_TOKEN' \$PROVISIONER_URL/healthz"Expected: Two {"status":"ok"} responses.
- [ ] Step 4: Test restricted policy 403 handling
Stop the first run (Ctrl+C), then:
./run.sh policies/restricted.yamlFrom inside the sandbox, try /push and /images — both should return 403 from the proxy.
- [ ] Step 5: Document findings
Create results/findings.md with observations from the smoke test:
Which endpoints worked, which returned 403
Latency observations
Any issues encountered with sandbox creation or policy application
In-sandbox build test results
[ ] Step 6: Commit findings
git add results/findings.md
git commit -m "docs: add initial experiment findings from smoke test"Plan complete and saved to experiments/host-side-api-server/superpowers/2026-05-14-host-side-api-server-plan.md. Two execution options:
1. Subagent-Driven (recommended) — I dispatch a fresh subagent per task, review between tasks, fast iteration
2. Inline Execution — Execute tasks in this session using executing-plans, batch execution with checkpoints
Which approach?
