docs/content/docs/tutorials/saas-file-writes-eic.md
molecule-ai[bot] a2a4d8b41d
docs(tutorials): add frontmatter to SaaS file writes EIC tutorial (#84)
Merging despite CI billing outage — build failure is infrastructure (account payment), not code. Content reviewed and approved.
2026-04-24 01:48:04 +00:00

6.1 KiB

title description
SaaS File Writes via EC2 Instance Connect How to use the Files API PUT endpoint to write files to SaaS (EC2-backed) workspaces via AWS EC2 Instance Connect.

When your workspace runs on a Molecule AI SaaS EC2 (not a Docker container), the Files API routes writes through AWS EC2 Instance Connect (EIC) — the same SSH-backed channel that powers the Terminal tab. This demo shows the end-to-end flow, the three routing decisions the handler makes, and how to verify the write succeeded.

Prerequisites

  • A Molecule AI SaaS workspace (EC2-backed — check instance_id in the workspace record)
  • curl + jq
  • AWS credentials with ec2-instance-connect:SendSSHPublicKey permission (usually handled by Molecule's runtime IAM role; you don't need to configure this yourself)

How the routing works

PUT /workspaces/:id/files/*path checks workspaces.instance_id before choosing a write path:

instance_id Write path
empty (self-hosted) Docker cp into running container, then offline ephemeral-container fallback
set (SaaS) EIC: SSH-backed write via aws ec2-instance-connect
Caller → PUT /workspaces/:id/files/config.yaml
                │
                ├─ Docker path (container running)
                │     copyFilesToContainer()
                │
                ├─ Docker path (container offline)
                │     writeViaEphemeral() → tar → docker run --rm -v …
                │
                └─ SaaS path (EC2 workspace)
                      writeFileViaEIC()
                      ├── ssh-keygen ed25519 (temp keypair)
                      ├── aws ec2-instance-connect send-ssh-public-key (60s window)
                      ├── aws ec2-instance-connect open-tunnel (local port → :22)
                      └── ssh ubuntu@127.0.0.1 -p LOCAL_PORT "install -D -m 0644 /dev/stdin ABS_PATH"

1 — List existing files

export WS_ID=<your-workspace-id>
export API_BASE=https://your-tenant.moleculesai.app   # or localhost:8080 for self-hosted

# List files under /configs (the default root)
curl -s "$API_BASE/workspaces/$WS_ID/files" \
  -H "Authorization: Bearer $ADMIN_TOKEN" | jq .

Sample response (SaaS / EC2 workspace):

[
  { "path": "config.yaml", "size": 412, "dir": false },
  { "path": "skills",     "size":   0, "dir": true  }
]

2 — Write a single file

curl -s -X PUT "$API_BASE/workspaces/$WS_ID/files/my-agent-prompt.md" \
  -H "Authorization: Bearer $ADMIN_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"content": "# Agent Prompt\n\nYou are a careful code reviewer."}' | jq .

Sample response:

{ "status": "saved", "path": "my-agent-prompt.md" }

What happened under the hood

  1. Handler looked up instance_id → not empty → routed to writeFileViaEIC()
  2. Ephemeral ed25519 keypair generated in /tmp/molecule-filewrite-*/
  3. Public key pushed via aws ec2-instance-connect send-ssh-public-key (valid 60 s)
  4. TLS tunnel opened on a local free port → workspace EC2 port 22
  5. ssh install -D -m 0644 /dev/stdin /home/ubuntu/.hermes/my-agent-prompt.md executed
  6. Keydir wiped on function return

3 — Bulk replace (multiple files at once)

curl -s -X PUT "$API_BASE/workspaces/$WS_ID/files" \
  -H "Authorization: Bearer $ADMIN_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "files": {
      "config.yaml": "name: my-agent\nversion: 1.0.0\ntier: 1\nmodel: anthropic:claude-sonnet-4-20250514\nskills: []\n",
      "rules.md":    "# Workspace Rules\n\nNo deletions without approval.",
      "system-prompt.md": "# System Prompt\n\nYou are a helpful coding assistant."
    }
  }' | jq .

Sample response:

{
  "status":    "replaced",
  "workspace": "a8af9d79-...",
  "files":     3,
  "source":    "ec2-ssh"
}

Bulk-write latency: Each file opens its own EIC tunnel (~3 s/file). For 10+ files consider writing a tar archive and extracting it in a single SSH session (follow-up tracked in the source PR).

4 — Verify the write landed on the EC2

# Read back via the Files API
curl -s "$API_BASE/workspaces/$WS_ID/files/my-agent-prompt.md" \
  -H "Authorization: Bearer $ADMIN_TOKEN" | jq .

# Confirm the absolute path on the EC2
# hermes runtime  → /home/ubuntu/.hermes/<relPath>
# langgraph       → /opt/configs/<relPath>
# external/unknown→ /opt/configs/<relPath>

Key security properties

Property How it's enforced
Path traversal blocked filepath.Clean(relPath) + .. prefix check; absolute paths rejected before any handler call
No shell injection Remote command uses install (not sh -c); absPath built from a closed map + Clean() only
Ephemeral credentials ed25519 keypair lives in tmpdir ≤ 30 s, wiped by defer RemoveAll
EIC 60 s key window AWS drops the temporary authorized key after 60 s regardless
OS user locked Reads from WORKSPACE_EC2_OS_USER env var (default ubuntu); no other user configurable

Error cases

500 failed to write file: workspace has no instance_id

The workspace record has no instance_id, meaning it is a self-hosted Docker workspace. For these, ensure the container is running or use the ephemeral-container fallback. This error only occurs on SaaS when instance_id is unexpectedly null.

500 path traversal blocked

relPath contained .. or was an absolute path. Rejecting at the API boundary before any file operation.

Timeout (30 s)

Key push + tunnel + write took longer than 30 s. Common causes: slow AWS EIC in the region, high SSH load on the target instance. Retry the request.

Source PR

PR #1702feat(files-api): SSH-backed write for SaaS workspaces (fixes 500 docker not available)

Key files in molecule-core:

  • workspace-server/internal/handlers/template_files_eic.go — EIC write logic
  • workspace-server/internal/handlers/template_import.goReplaceFiles SaaS routing
  • workspace-server/internal/handlers/templates.goWriteFile Docker → EIC routing